diff --git a/clients/banking/src/components/TransferInternationalWizard.tsx b/clients/banking/src/components/TransferInternationalWizard.tsx index d1662cd09..ab265fa81 100644 --- a/clients/banking/src/components/TransferInternationalWizard.tsx +++ b/clients/banking/src/components/TransferInternationalWizard.tsx @@ -9,7 +9,16 @@ import { useState } from "react"; import { ScrollView, StyleSheet, View } from "react-native"; import { match } from "ts-pattern"; import { t } from "../utils/i18n"; -import { Amount, TransferInternationalWizardAmount } from "./TransferInternationalWizardAmount"; +import { + Amount, + TransferInternationalWizardAmount, + TransferInternationamWizardAmountSummary, +} from "./TransferInternationalWizardAmount"; +import { + Beneficiary, + TransferInternationalWizardBeneficiary, +} from "./TransferInternationalWizardBeneficiary"; +import { Details, TransferInternationalWizardDetails } from "./TransferInternationalWizardDetails"; const styles = StyleSheet.create({ root: { @@ -52,12 +61,10 @@ const styles = StyleSheet.create({ }, }); -// [NC] FIXME -type Beneficiary = string; - type Step = | { name: "Amount"; amount?: Amount } - | { name: "Beneficiary"; amount: Amount; beneficiary?: Beneficiary }; + | { name: "Beneficiary"; amount: Amount; beneficiary?: Beneficiary } + | { name: "Details"; amount: Amount; beneficiary: Beneficiary; details?: Details }; type Props = { onPressClose: () => void; @@ -65,6 +72,9 @@ type Props = { accountMembershipId: string; }; +// [NC] FIXME +const onSave = console.log; + export const TransferInternationalWizard = ({ onPressClose, accountId, @@ -121,6 +131,50 @@ export const TransferInternationalWizard = ({ ); }) + .with({ name: "Beneficiary" }, ({ amount, beneficiary }) => { + return ( + <> + setStep({ name: "Amount", amount })} + /> + + + + + {t("transfer.new.internationalTransfer.beneficiary.title")} + + + + + setStep({ name: "Amount", amount })} + onSave={beneficiary => setStep({ name: "Details", amount, beneficiary })} + /> + + ); + }) + .with({ name: "Details" }, ({ amount, beneficiary, details }) => { + return ( + <> + + {t("transfer.new.internationalTransfer.details.title")} + + + + + + onSave({ name: "Beneficiary", amount, beneficiary, details }) + } + /> + + ); + }) .otherwise(() => null)} diff --git a/clients/banking/src/components/TransferInternationalWizardAmount.tsx b/clients/banking/src/components/TransferInternationalWizardAmount.tsx index a13c1b7cc..3aeda2f45 100644 --- a/clients/banking/src/components/TransferInternationalWizardAmount.tsx +++ b/clients/banking/src/components/TransferInternationalWizardAmount.tsx @@ -1,4 +1,6 @@ import { AsyncData, Result } from "@swan-io/boxed"; +import { Box } from "@swan-io/lake/src/components/Box"; +import { Fill } from "@swan-io/lake/src/components/Fill"; import { LakeButton, LakeButtonGroup } from "@swan-io/lake/src/components/LakeButton"; import { LakeHeading } from "@swan-io/lake/src/components/LakeHeading"; import { LakeLabel } from "@swan-io/lake/src/components/LakeLabel"; @@ -13,7 +15,7 @@ import { useUrqlQuery } from "@swan-io/lake/src/hooks/useUrqlQuery"; import { isNullish } from "@swan-io/lake/src/utils/nullish"; import { useEffect, useState } from "react"; import { ActivityIndicator, View } from "react-native"; -import { useForm } from "react-ux-form"; +import { hasDefinedKeys, useForm } from "react-ux-form"; import { P, match } from "ts-pattern"; import { GetAvailableAccountBalanceDocument, @@ -53,14 +55,12 @@ export const TransferInternationalWizardAmount = ({ { query: GetInternationalCreditTransferQuoteDocument, variables: input ?? { value: "", currency: "" }, - pause: !input || input?.value === "0" || Number.isNaN(Number(input?.value)) - , + pause: !input || input?.value === "0" || Number.isNaN(Number(input?.value)), }, [input], ); - - const { Field, getFieldState, submitForm, listenFields } = useForm({ + const { Field, submitForm, listenFields } = useForm({ amount: { initialValue: initialAmount ?? { value: FIXED_AMOUNT_DEFAULT_VALUE, @@ -152,7 +152,8 @@ export const TransferInternationalWizardAmount = ({ .with(AsyncData.P.NotAsked, () => null) .with(AsyncData.P.Loading, () => ( <> - + + )) .with( @@ -166,7 +167,7 @@ export const TransferInternationalWizardAmount = ({ <> {formatNestedMessage("transfer.new.internationalTransfer.amount.description", { - amount: `${q.sourceAmount.value} ${q.sourceAmount.currency}`, + amount: formatCurrency(Number(q.sourceAmount.value), q.sourceAmount.currency), rate: q.exchangeRate, bold: str => ( @@ -180,7 +181,7 @@ export const TransferInternationalWizardAmount = ({ {formatNestedMessage("transfer.new.internationalTransfer.fee", { - fee: `${q.feesAmount.value} ${q.feesAmount.currency}`, + fee: formatCurrency(Number(q.feesAmount.value), q.feesAmount.currency), bold: str => ( {str} @@ -195,9 +196,10 @@ export const TransferInternationalWizardAmount = ({ {formatNestedMessage("transfer.new.internationalTransfer.amount.converted", { - amount: `${( - parseFloat(q.feesAmount.value) + parseFloat(q.sourceAmount.value) - ).toFixed(2)} ${q.sourceAmount.currency}`, + amount: formatCurrency( + parseFloat(q.feesAmount.value) + parseFloat(q.sourceAmount.value), + q.sourceAmount.currency, + ), colored: str => ( {str} @@ -223,7 +225,20 @@ export const TransferInternationalWizardAmount = ({ {t("common.cancel")} - onSave(amount)} grow={small}> + + submitForm(values => { + if (hasDefinedKeys(values, ["amount"])) { + onSave({ + value: values.amount.value, + currency: values.amount.currency as Currency, + }); + } + }) + } + grow={small} + > {t("common.continue")} @@ -232,3 +247,34 @@ export const TransferInternationalWizardAmount = ({ ); }; + +type SummaryProps = { + amount: Amount; + onPressEdit: () => void; +}; + +export const TransferInternationamWizardAmountSummary = ({ amount, onPressEdit }: SummaryProps) => { + return ( + + + + + {t("transfer.new.internationalTransfer.amount.summary.title")} + + + + + + {formatCurrency(Number(amount.value), amount.currency)} + + + + + + + {t("common.edit")} + + + + ); +}; diff --git a/clients/banking/src/components/TransferInternationalWizardBeneficiary.tsx b/clients/banking/src/components/TransferInternationalWizardBeneficiary.tsx new file mode 100644 index 000000000..64d152531 --- /dev/null +++ b/clients/banking/src/components/TransferInternationalWizardBeneficiary.tsx @@ -0,0 +1,214 @@ +import { AsyncData, Result } from "@swan-io/boxed"; +import { LakeButton, LakeButtonGroup } from "@swan-io/lake/src/components/LakeButton"; +import { LakeLabel } from "@swan-io/lake/src/components/LakeLabel"; +import { LakeSelect } from "@swan-io/lake/src/components/LakeSelect"; +import { LakeTextInput } from "@swan-io/lake/src/components/LakeTextInput"; +import { RadioGroup } from "@swan-io/lake/src/components/RadioGroup"; +import { ResponsiveContainer } from "@swan-io/lake/src/components/ResponsiveContainer"; +import { Space } from "@swan-io/lake/src/components/Space"; +import { Tile } from "@swan-io/lake/src/components/Tile"; +import { colors } from "@swan-io/lake/src/constants/design"; +import { useUrqlQuery } from "@swan-io/lake/src/hooks/useUrqlQuery"; +import { isNullishOrEmpty } from "@swan-io/lake/src/utils/nullish"; +import { useEffect, useMemo, useState } from "react"; +import { ActivityIndicator, View } from "react-native"; +import { useForm } from "react-ux-form"; +import { P, match } from "ts-pattern"; +import { + Field as DynamicFormField, + GetInternationalBeneficiaryDynamicFormsDocument, + InternationalCreditTransferDisplayLanguage, +} from "../graphql/partner"; +import { locale, t } from "../utils/i18n"; +import { validateRequired } from "../utils/validations"; +import { Amount } from "./TransferInternationalWizardAmount"; + +export type Beneficiary = { name: string }; + +type Props = { + initialBeneficiary?: Beneficiary; + amount: Amount; + onPressPrevious: () => void; + onSave: (details: Beneficiary) => void; +}; + +export const TransferInternationalWizardBeneficiary = ({ + initialBeneficiary, + amount, + onPressPrevious, + onSave, +}: Props) => { + const { data } = useUrqlQuery( + { + query: GetInternationalBeneficiaryDynamicFormsDocument, + variables: { + // [NC] FIXME + dynamicFields: [], + amountValue: amount.value, + currency: amount.currency, + language: locale.language.toUpperCase() as InternationalCreditTransferDisplayLanguage, + }, + }, + [locale.language], + ); + + const { Field } = useForm({ + name: { + initialValue: initialBeneficiary?.name, + sanitize: () => {}, + validate: () => {}, + }, + }); + + console.log("[NC] data", data); + + return ( + + + ( + + {({ value, onChange, onBlur, error, valid, ref }) => ( + + )} + + )} + /> + + {match(data) + .with(AsyncData.P.NotAsked, () => null) + .with( + AsyncData.P.Done(Result.P.Ok(P.select())), + ({ internationalBeneficiaryDynamicForms: { schemes } }) => { + if (isNullishOrEmpty(schemes)) { + return null; + } + + return ; + }, + ) + .otherwise(() => ( + + ))} + + + + + + {({ small }) => ( + + + {t("common.previous")} + + + onSave(beneficiary)} grow={small}> + {t("common.continue")} + + + )} + + + ); +}; + +type BeneficiaryFormProps = { + schemes: { + fields: DynamicFormField[]; + remainingFieldsToRefreshCount: number; + title: string; + type: string; + }[]; +}; + +const BeneficiaryForm = ({ schemes }: BeneficiaryFormProps) => { + const [route, setRoute] = useState(); + const routes = useMemo<{ value: string; name: string }[]>( + () => schemes?.map(({ type: value, title: name }) => ({ value, name })) ?? [], + [schemes], + ); + const fields = useMemo(() => schemes.find(({ type }) => type === route)?.fields ?? [], [route]); + + const { Field, listenFields } = useForm( + fields.reduce((acc, current) => { + acc[current.key] = { + initialValue: "test", + validate: current.required ? validateRequired : () => "", + }; + return acc; + }, {}), + ); + + useEffect( + () => + listenFields( + fields.map(({ key }) => key), + t => console.log("[NC] t", t), + ), + [fields, listenFields], + ); + + useEffect(() => { + if (isNullishOrEmpty(route)) { + setRoute(routes?.[0]?.value); + } + }, [routes]); + + return ( + <> + + + + {fields.length && + fields.map(({ name, key, ...field }, i) => ( + + match(field) + // .with({ __typename: "SelectField" }, ({ allowedValues }) => ( + // + // {({ onChange, value, ref }) => ( + // ({ name, value }))} + // value={value} + // onValueChange={onChange} + // /> + // )} + // + // )) + // .with({ __typename: "DateField" }, () => DateField not implemented) + // .with({ __typename: "RadioField" }, () => RadioField not implemented) + .otherwise(() => ( + + {({ value, onChange, onBlur, error, valid, ref }) => ( + + )} + + )) + } + /> + ))} + + ); +}; diff --git a/clients/banking/src/components/TransferInternationalWizardDetails.tsx b/clients/banking/src/components/TransferInternationalWizardDetails.tsx new file mode 100644 index 000000000..467783402 --- /dev/null +++ b/clients/banking/src/components/TransferInternationalWizardDetails.tsx @@ -0,0 +1,51 @@ +import { LakeButton, LakeButtonGroup } from "@swan-io/lake/src/components/LakeButton"; +import { ResponsiveContainer } from "@swan-io/lake/src/components/ResponsiveContainer"; +import { Space } from "@swan-io/lake/src/components/Space"; +import { Tile } from "@swan-io/lake/src/components/Tile"; +import { View } from "react-native"; +import { useForm } from "react-ux-form"; +import { t } from "../utils/i18n"; + +export type Details = { label: string; reference: string }; + +type Props = { + initialDetails?: Details; + onPressPrevious: () => void; + onSave: (details: Details) => void; +}; + +export const TransferInternationalWizardDetails = ({ + initialDetails, + onPressPrevious, + onSave, +}: Props) => { + const { Field, getFieldState, submitForm, listenFields } = useForm({ + details: { + initialValue: null, + sanitize: () => {}, + validate: () => {}, + }, + }); + + return ( + + + + + + + {({ small }) => ( + + + {t("common.previous")} + + + {}} grow={small}> + {t("common.confirm")} + + + )} + + + ); +}; diff --git a/clients/banking/src/graphql/partner.gql b/clients/banking/src/graphql/partner.gql index 1cc877eee..ab3457e46 100644 --- a/clients/banking/src/graphql/partner.gql +++ b/clients/banking/src/graphql/partner.gql @@ -1935,6 +1935,70 @@ query GetInternationalCreditTransferQuote($value: AmountValue!, $currency: Curre } } +query GetInternationalBeneficiaryDynamicForms( + $amountValue: AmountValue! + $currency: Currency! + $language: InternationalCreditTransferDisplayLanguage! + $dynamicFields: [InternationalBeneficiaryDetailsInput!] +) { + internationalBeneficiaryDynamicForms( + amount: { value: $amountValue, currency: $currency } + language: $language + dynamicFields: $dynamicFields + ) { + schemes { + type + title + remainingFieldsToRefreshCount + fields { + key + name + required + ... on DateField { + example + validationRegex + key + name + required + } + ... on TextField { + displayFormat + example + validationRegex + required + refreshDynamicFieldsOnChange + name + minLength + maxLength + key + } + ... on SelectField { + __typename + required + refreshDynamicFieldsOnChange + name + key + allowedValues { + name + key + } + } + ... on RadioField { + __typename + required + refreshDynamicFieldsOnChange + name + key + allowedValues { + name + key + } + } + } + } + } +} + # Mutations mutation CreateMultiConsent($input: CreateMultiConsentInput!) { diff --git a/clients/banking/src/locales/en.json b/clients/banking/src/locales/en.json index a49bc26b7..774b42813 100644 --- a/clients/banking/src/locales/en.json +++ b/clients/banking/src/locales/en.json @@ -236,6 +236,7 @@ "common.change": "Change", "common.chooseFilter": "Choose filters", "common.closeButton": "Close", + "common.confirm": "Confirm", "common.continue": "Continue", "common.edit": "Edit", "common.empty": "Empty", @@ -724,12 +725,18 @@ "transfer.new.internationalTransfer.title": "New international transfer", "transfer.new.internationalTransfer.amount.title": "Enter details about your transfer", + "transfer.new.internationalTransfer.amount.summary.title": "You're sending", "transfer.new.internationalTransfer.amount.label": "Beneficiary gets", "transfer.new.internationalTransfer.amount.description": "Total amount we'll convert {amount} (Exchange rate {rate})", "transfer.new.internationalTransfer.fee": "Transfer fee {fee}", "transfer.new.internationalTransfer.amount.converted": "You send exactly {amount}", + "transfer.new.internationalTransfer.beneficiary.title": "Who are you sending money to?", + "transfer.new.internationalTransfer.beneficiary.name": "Beneficiary name", + "transfer.new.internationalTransfer.details.title": "Transfer details", + + "transfer.new.title": "New transfer", "transfer.newRecurringTransfer": "New standing order", "transfer.newTransfer": "New transfer",