Skip to content

Commit

Permalink
BG-1019: Bank form to use new wise-proxy (#2631)
Browse files Browse the repository at this point in the history
* container

* requirements file

* wise currencies

* expected funds

* requirements query

* types

* fields

* order to preserve state

* validation error

* async validation

* radio type

* also return quote id

* handle overflow

* refresh endpoint

* styles

* required

* dual optional

* focus

* date

* style payment option

* rename type

* uniform style

* remove popover

* remove unused

* use component classes

* name

* open sans

* load text

* required amount

* create recipient

* remove font-work

* unregister to remove residual state

* bank statement input

* bank statement file

* remove unused

* prune props

* move currency selector

* update wise currencies

* rename

* expected funds

* clean bank details

* Recipient details form

* recipient details form

* remove unused

* remove unused

* currency selector

* pass form buttons

* setup onsubmit

* use in registration

* use updated

* finished and reset

* fix error

* get file previews

* updates

* navigate

* remove unused service

* remove unused

* disabled form

* delay focus

* remove disabled
  • Loading branch information
ap-justin committed Jan 8, 2024
1 parent e6f8a08 commit 3f9110d
Show file tree
Hide file tree
Showing 38 changed files with 691 additions and 1,972 deletions.
103 changes: 21 additions & 82 deletions src/components/BankDetails/BankDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,52 @@
import { ComponentType, useState } from "react";
import { FormButtonsProps } from "./types";
import { CreateRecipientRequest } from "types/aws";
import { FileDropzoneAsset } from "types/components";
import { useState } from "react";
import { IFormButtons, OnSubmit } from "./types";
import { WiseCurrency } from "types/aws";
import Divider from "components/Divider";
import LoaderRing from "components/LoaderRing";
import useDebounce from "hooks/useDebounce";
import { isEmpty } from "helpers";
import { GENERIC_ERROR_MESSAGE } from "constants/common";
import useDebouncer from "hooks/useDebouncer";
import CurrencySelector from "./CurrencySelector";
import ExpectedFunds from "./ExpectedFunds";
import RecipientDetails from "./RecipientDetails";
import UpdateDetailsButton from "./UpdateDetailsButton";
import useCurrencies from "./useCurrencies";

/**
* Denominated in USD
*/
const DEFAULT_EXPECTED_MONTHLY_DONATIONS_AMOUNT = 1000;
const DEFAULT_EXPECTED_MONTHLY_DONATIONS_AMOUNT = "1000";

type Props = {
shouldUpdate: boolean;
onInitiateUpdate: () => void;
isSubmitting: boolean;
FormButtons: ComponentType<FormButtonsProps>;
onSubmit: (
request: CreateRecipientRequest,
bankStatementFile: FileDropzoneAsset,
isDirty: boolean
) => Promise<void>;
FormButtons: IFormButtons;
onSubmit: OnSubmit;
};

export default function BankDetails({
shouldUpdate,
onInitiateUpdate,
...props
}: Props) {
if (!shouldUpdate) {
return (
<div className="flex flex-col w-full justify-between mt-8 max-md:items-center">
<UpdateDetailsButton onClick={onInitiateUpdate} />
<props.FormButtons disabled refreshRequired isSubmitted />
</div>
);
}

return <Content {...props} />;
}

function Content({
FormButtons,
isSubmitting,
onSubmit,
}: Omit<Props, "shouldUpdate" | "onInitiateUpdate">) {
// the initial/default value will never be displayed, as ExpectedFunds displays its own internal value (empty by default)
const [expectedMontlyDonations, setExpectedMontlyDonations] = useState(
export default function BankDetails({ FormButtons, onSubmit }: Props) {
const [currency, setCurrency] = useState<Pick<WiseCurrency, "code" | "name">>(
{ code: "USD", name: "United States Dollar" }
);
const [amount, setAmount] = useState(
DEFAULT_EXPECTED_MONTHLY_DONATIONS_AMOUNT
);

const [debounce, isDebouncing] = useDebounce();

const { currencies, isLoading, targetCurrency, setTargetCurrency } =
useCurrencies();

if (isLoading) {
return (
<div className="flex items-center gap-2">
<LoaderRing thickness={10} classes="w-6" /> Loading...
</div>
);
}

if (isEmpty(currencies) || !targetCurrency) {
return <span>{GENERIC_ERROR_MESSAGE}</span>;
}
const [debouncedAmount] = useDebouncer(amount, 500);
const amnt = /^[1-9]\d*$/.test(debouncedAmount) ? +debouncedAmount : 0;

return (
<div className="grid gap-6">
<CurrencySelector
value={targetCurrency}
currencies={currencies}
onChange={setTargetCurrency}
onChange={(c) => setCurrency(c)}
value={currency}
classes={{ combobox: "w-full md:w-80" }}
disabled={isSubmitting}
/>
<ExpectedFunds
value={amount}
onChange={(amount) => setAmount(amount)}
classes={{ input: "md:w-80" }}
disabled={isSubmitting}
onChange={(value) => {
// if value is empty or 0 (zero), use the default value so that there's some form to show
const newValue = value || DEFAULT_EXPECTED_MONTHLY_DONATIONS_AMOUNT;
// if new value is the same as the current value, then there's no need to debounce,
// but still call the function to cancel the previous debounce call
const delay = newValue === expectedMontlyDonations ? 0 : 1000;
debounce(() => setExpectedMontlyDonations(newValue), delay);
}}
/>

<Divider />

<RecipientDetails
// we need this key to tell React that when currency code changes,
// it needs to reset its state by re-rendering the whole component.
key={targetCurrency.code}
isLoading={isDebouncing}
currency={targetCurrency}
expectedMontlyDonations={expectedMontlyDonations}
isSubmitting={isSubmitting}
onSubmit={onSubmit}
amount={amnt}
currency={currency.code}
FormButtons={FormButtons}
onSubmit={onSubmit}
/>
</div>
);
Expand Down
56 changes: 56 additions & 0 deletions src/components/BankDetails/CurrencyOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Combobox } from "@headlessui/react";
import { useCurrencisQuery } from "services/aws/wise";
import { isEmpty } from "helpers";

type Props = {
classes?: string;
searchText?: string;
};

export default function Options({ classes = "", searchText = "" }: Props) {
const { data: currencies = [] } = useCurrencisQuery(
{},
{
selectFromResult({ data = [], ...rest }) {
return {
data: data.filter((c) => {
// check whether query matches either the currency name or any of its keywords
const formatQuery = searchText.toLowerCase().replace(/\s+/g, ""); // ignore spaces and casing
const matchesCode = c.code.toLowerCase().includes(formatQuery);
const matchesName = c.name
.toLowerCase()
.replace(/\s+/g, "") // ignore spaces and casing
.includes(formatQuery);

return matchesCode || matchesName;
}),
...rest,
};
},
}
);

return (
<Combobox.Options
className={`${classes} w-full bg-white dark:bg-blue-d6 shadow-lg rounded max-h-52 overflow-y-auto scroller text-base ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm`}
>
{isEmpty(currencies) ? (
<div className="p-2 text-sm cursor-default">Nothing found</div>
) : (
currencies.map(({ code, name }) => (
<Combobox.Option key={code} value={{ code, name }}>
{({ active, selected }) => (
<div
className={`${active ? "bg-blue-l2 dark:bg-blue-d1" : ""} ${
selected ? "font-semibold" : "font-normal"
} flex items-center gap-2 p-2 text-sm cursor-pointer truncate`}
>
{code} - {name}
</div>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
);
}
74 changes: 11 additions & 63 deletions src/components/BankDetails/CurrencySelector.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Combobox, Transition } from "@headlessui/react";
import { Fragment, memo, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { useState } from "react";
import { WiseCurrency } from "types/aws";
import { DrawerIcon } from "components/Icon";
import { Label } from "components/form";
import { isEmpty } from "helpers";
import CurrencyOptions from "./CurrencyOptions";

export type Currency = {
code: string;
Expand All @@ -12,50 +12,27 @@ export type Currency = {

type Props = {
classes: { combobox: string };
disabled: boolean;
value: Currency;
currencies: Currency[];
onChange: (currency: Currency) => void;
};

function currencyFilter(query: string): (value: Currency) => boolean {
return (currency) => {
// check whether query matches either the currency name or any of its keywords
const formatQuery = query.toLowerCase().replace(/\s+/g, ""); // ignore spaces and casing
const matchesCode = currency.code.toLowerCase().includes(formatQuery);
const matchesName = currency.name
.toLowerCase()
.replace(/\s+/g, "") // ignore spaces and casing
.includes(formatQuery);

return matchesCode || matchesName;
};
}

function CurrencySelector(props: Props) {
const inputRef = useRef<HTMLInputElement>(null);
export default function CurrencySelector(props: Props) {
const [query, setQuery] = useState("");

const filteredCurrencies =
query === ""
? props.currencies
: props.currencies.filter(currencyFilter(query));

return (
<div className="field">
<Label htmlFor={inputRef.current?.id} required aria-required>
<Label htmlFor="wise__currency" required aria-required>
Select your bank account currency:
</Label>
<Combobox
by="code"
value={props.value}
aria-disabled={props.disabled}
disabled={props.disabled}
onChange={props.onChange}
as="div"
className={`relative items-center grid grid-cols-[1fr_auto] field-container ${props.classes.combobox}`}
>
<Combobox.Input
ref={inputRef}
id="wise__currency"
className="w-full border-r border-gray-l3 dark:border-bluegray px-4 py-3.5 text-sm leading-5 text-gray-900 focus:ring-0"
displayValue={(currency: WiseCurrency) =>
`${currency.code} - ${currency.name}`
Expand All @@ -74,40 +51,11 @@ function CurrencySelector(props: Props) {
)}
</Combobox.Button>

<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={() => setQuery("")}
>
<Combobox.Options className="absolute top-full mt-2 z-10 w-full bg-white dark:bg-blue-d6 shadow-lg rounded max-h-52 overflow-y-auto scroller text-base ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{isEmpty(filteredCurrencies) ? (
<div className="p-2 text-sm cursor-default">Nothing found</div>
) : (
filteredCurrencies.map((currency) => (
<Combobox.Option key={currency.code} value={currency}>
{({ active, selected }) => (
<div
className={`${
active ? "bg-blue-l2 dark:bg-blue-d1" : ""
} ${
selected ? "font-semibold" : "font-normal"
} flex items-center gap-2 p-2 text-sm cursor-pointer truncate`}
>
{currency.code} - {currency.name}
</div>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Transition>
<CurrencyOptions
classes="absolute top-full mt-2 z-10"
searchText={query}
/>
</Combobox>
</div>
);
}

// Should only ever re-render when a new currency is selected,
// not when some unrelated state changes
export default memo(CurrencySelector);
66 changes: 18 additions & 48 deletions src/components/BankDetails/ExpectedFunds.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,39 @@
import { Popover, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
import Icon from "components/Icon";
import { Label } from "components/form";
import { APP_NAME } from "constants/env";

type Props = {
classes: { input: string };
disabled: boolean;
onChange: (expectedFunds: number) => void;
onChange: (amount: string) => void;
value: string;
disabled?: boolean;
};

export default function ExpectedFunds(props: Props) {
const [value, setValue] = useState("");

return (
<div className="field">
<div className="flex sm:gap-2 items-center mb-1">
<Label htmlFor="amount">
What is the amount of donations (in USD) you expect to receive monthly
on our platform?
</Label>
<Popover className="relative">
<>
<Popover.Button className="group flex items-center rounded-full text-base font-medium hover:text-orange focus:outline-none">
<Icon type="Info" className="text-2xl" />
</Popover.Button>
{/** Transition is configured so that the popover appears from the top on smaller screens and from the bottom on larger screens*/}
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-0 lg:translate-y-1"
enterTo="opacity-100 translate-y-1 lg:translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-1 lg:translate-y-0"
leaveTo="opacity-0 translate-y-0 lg:translate-y-1"
>
{/** using `-translate-x-2/4` instead of `-translate-x-1/2` here as the latter occasionally has no effect for some reason */}
<Popover.Panel className="absolute -left-6 -translate-x-3/4 lg:-translate-x-2/4 lg:-top-40 mt-1 sm:mt-0 z-10 w-screen max-w-xs sm:max-w-sm p-4 bg-white dark:bg-blue-d3 overflow-hidden text-xs sm:text-sm rounded-lg shadow-lg ring-1 ring-black/5">
Depending on how much you expect to receive each month via{" "}
{APP_NAME}, different details are required. At this point, we
recommend using a conservative figure - Maybe $1000 per month.
</Popover.Panel>
</Transition>
</>
</Popover>
</div>
<Label htmlFor="wise__amount" required>
What is the amount of donations (in USD) you expect to receive monthly
on our platform?
</Label>
<input
id="amount"
id="wise__amount"
type="text"
value={value}
value={props.value}
pattern="^[1-9]\d*$"
required
placeholder="1,000"
onChange={(event) => {
const tvalue = Number(event.target.value);
if (isNaN(tvalue)) {
return event.preventDefault();
}
setValue(event.target.value);
props.onChange(tvalue);
}}
className={`field-input text-field ${props.classes.input}`}
onChange={(event) => props.onChange(event.target.value)}
className={`field-input text-field ${props.classes.input} invalid:ring-1 invalid:ring-red`}
autoComplete="off"
spellCheck={false}
disabled={props.disabled}
inputMode="numeric"
/>
<p className="text-gray-d1 text-sm my-2 italic">
Depending on how much you expect to receive each month via {APP_NAME},
different details are required. At this point, we recommend using a
conservative figure - Maybe $1000 per month.
</p>
</div>
);
}
Loading

0 comments on commit 3f9110d

Please sign in to comment.