diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 7d37ca62ac30..c7e79f04b734 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -738,6 +738,8 @@ const CONST = { // The back-end is always returning account number with 4 last digits and mask the rest with X MASKED_US_ACCOUNT_NUMBER: /^[X]{0,13}[0-9]{4}$/, SWIFT_BIC: /^[A-Za-z0-9]{8,11}$/, + IBAN: /^[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}([a-zA-Z0-9]?){0,16}$/, + INTERNATIONAL_SWIFT_CODE: /^.{1,12}$/, }, STATE: { VERIFYING: 'VERIFYING', @@ -7972,6 +7974,7 @@ const CONST = { PAGE_NAME: { COUNTRY: 'country', ACCOUNT_DETAILS: 'account-details', + INTERNATIONAL_BANK_ACCOUNT_DETAILS: 'international-bank-account-details', ACCOUNT_TYPE: 'account-type', BANK_INFORMATION: 'bank-information', ACCOUNT_HOLDER_DETAILS: 'account-holder-details', @@ -7982,11 +7985,12 @@ const CONST = { MAPPING: { COUNTRY_SELECTOR: 0, BANK_ACCOUNT_DETAILS: 1, - ACCOUNT_TYPE: 2, - BANK_INFORMATION: 3, - ACCOUNT_HOLDER_INFORMATION: 4, - CONFIRMATION: 5, - SUCCESS: 6, + INTERNATIONAL_BANK_ACCOUNT_DETAILS: 2, + ACCOUNT_TYPE: 3, + BANK_INFORMATION: 4, + ACCOUNT_HOLDER_INFORMATION: 5, + CONFIRMATION: 6, + SUCCESS: 7, }, }, }, diff --git a/src/languages/de.ts b/src/languages/de.ts index ab7159789b5f..a2d7aacb9f4f 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3636,6 +3636,9 @@ ${amount} für ${merchant} – ${date}`, thisBankAccount: 'Dieses Bankkonto wird für Geschäftszahlungen in deinem Workspace verwendet', accountNumber: 'Kontonummer', routingNumber: 'Routing-Nummer', + internationalBankAccountDetails: 'Internationale Bankkontodaten', + iban: 'IBAN', + swiftBicCode: 'SWIFT/BIC-Code', chooseAnAccountBelow: 'Wähle unten ein Konto aus', addBankAccount: 'Bankkonto hinzufügen', chooseAnAccount: 'Konto auswählen', @@ -3686,6 +3689,7 @@ ${amount} für ${merchant} – ${date}`, restrictedBusiness: 'Bitte bestätigen Sie, dass das Unternehmen nicht auf der Liste der eingeschränkten Unternehmen steht.', routingNumber: 'Bitte geben Sie eine gültige Bankleitzahl ein', accountNumber: 'Bitte eine gültige Kontonummer eingeben', + swiftCodeOrIban: 'Bitte gib einen gültigen Swift-Code/IBAN ein', routingAndAccountNumberCannotBeSame: 'Bankleitzahl und Kontonummer dürfen nicht identisch sein', companyType: 'Bitte wähle einen gültigen Unternehmenstyp aus', tooManyAttempts: diff --git a/src/languages/en.ts b/src/languages/en.ts index b38453141ed1..6c87a7f7efe2 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3734,6 +3734,9 @@ const translations = { thisBankAccount: 'This bank account will be used for business payments on your workspace', accountNumber: 'Account number', routingNumber: 'Routing number', + internationalBankAccountDetails: 'International bank account details', + iban: 'IBAN', + swiftBicCode: 'SWIFT/BIC code', chooseAnAccountBelow: 'Choose an account below', addBankAccount: 'Add bank account', chooseAnAccount: 'Choose an account', @@ -3783,6 +3786,7 @@ const translations = { restrictedBusiness: "Please confirm the business isn't on the list of restricted businesses", routingNumber: 'Please enter a valid routing number', accountNumber: 'Please enter a valid account number', + swiftCodeOrIban: 'Please enter a valid Swift code/IBAN', routingAndAccountNumberCannotBeSame: "Routing and account numbers can't match", companyType: 'Please select a valid company type', tooManyAttempts: 'Due to a high number of login attempts, this option has been disabled for 24 hours. Please try again later or enter details manually instead.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 80f5e64f2d4c..72489554684e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3508,6 +3508,9 @@ ${amount} para ${merchant} - ${date}`, thisBankAccount: 'Esta cuenta bancaria se utilizará para pagos comerciales en tu espacio de trabajo', accountNumber: 'Número de cuenta', routingNumber: 'Número de ruta', + internationalBankAccountDetails: 'Datos de la cuenta bancaria internacional', + iban: 'IBAN', + swiftBicCode: 'Código SWIFT/BIC', chooseAnAccountBelow: 'Elige una cuenta a continuación', addBankAccount: 'Añadir cuenta bancaria', chooseAnAccount: 'Elige una cuenta', @@ -3557,6 +3560,7 @@ ${amount} para ${merchant} - ${date}`, restrictedBusiness: 'Por favor, confirma que la empresa no está en la lista de negocios restringidos', routingNumber: 'Por favor, introduce un número de ruta válido', accountNumber: 'Por favor, introduce un número de cuenta válido', + swiftCodeOrIban: 'Por favor, introduce un código Swift/IBAN válido', routingAndAccountNumberCannotBeSame: 'Los números de ruta y de cuenta no pueden ser iguales', companyType: 'Por favor, selecciona un tipo de compañía válido', tooManyAttempts: diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2c80ad56187f..abf21cf6a17c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3646,6 +3646,9 @@ ${amount} pour ${merchant} - ${date}`, thisBankAccount: 'Ce compte bancaire sera utilisé pour les paiements professionnels dans votre espace de travail', accountNumber: 'Numéro de compte', routingNumber: 'Numéro de routage', + internationalBankAccountDetails: 'Coordonnées bancaires internationales', + iban: 'IBAN', + swiftBicCode: 'Code SWIFT/BIC', chooseAnAccountBelow: 'Choisissez un compte ci-dessous', addBankAccount: 'Ajouter un compte bancaire', chooseAnAccount: 'Choisissez un compte', @@ -3696,6 +3699,7 @@ ${amount} pour ${merchant} - ${date}`, restrictedBusiness: 'Veuillez confirmer que l’entreprise ne figure pas sur la liste des entreprises restreintes', routingNumber: 'Veuillez saisir un numéro d’acheminement valide', accountNumber: 'Veuillez saisir un numéro de compte valide', + swiftCodeOrIban: 'Veuillez saisir un code Swift/IBAN valide', routingAndAccountNumberCannotBeSame: 'Le code de routage et le numéro de compte ne peuvent pas être identiques', companyType: 'Veuillez sélectionner un type d’entreprise valide', tooManyAttempts: diff --git a/src/languages/it.ts b/src/languages/it.ts index c6cce232c27a..73a344d03e2c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3627,6 +3627,9 @@ ${amount} per ${merchant} - ${date}`, thisBankAccount: 'Questo conto bancario sarà utilizzato per i pagamenti aziendali nel tuo spazio di lavoro', accountNumber: 'Numero di conto', routingNumber: 'Numero di instradamento', + internationalBankAccountDetails: 'Dati del conto bancario internazionale', + iban: 'IBAN', + swiftBicCode: 'Codice SWIFT/BIC', chooseAnAccountBelow: 'Scegli un account qui sotto', addBankAccount: 'Aggiungi conto bancario', chooseAnAccount: 'Scegli un account', @@ -3676,6 +3679,7 @@ ${amount} per ${merchant} - ${date}`, restrictedBusiness: 'Conferma che l’azienda non sia nell’elenco delle attività soggette a restrizioni', routingNumber: 'Inserisci un numero di instradamento valido', accountNumber: 'Inserisci un numero di conto valido', + swiftCodeOrIban: 'Inserisci un codice Swift/IBAN valido', routingAndAccountNumberCannotBeSame: 'I numeri di routing e di conto non possono coincidere', companyType: 'Seleziona un tipo di azienda valido', tooManyAttempts: 'A causa di un numero elevato di tentativi di accesso, questa opzione è stata disabilitata per 24 ore. Riprova più tardi o inserisci invece i dati manualmente.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index cd0d1dd46a70..353967b497a2 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3594,6 +3594,9 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' thisBankAccount: 'この銀行口座は、ワークスペースでのビジネス支払いに使用されます', accountNumber: '口座番号', routingNumber: 'ルーティング番号', + internationalBankAccountDetails: '海外銀行口座情報', + iban: 'IBAN', + swiftBicCode: 'SWIFT/BICコード', chooseAnAccountBelow: '下のアカウントを選択してください', addBankAccount: '銀行口座を追加', chooseAnAccount: 'アカウントを選択', @@ -3643,6 +3646,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' restrictedBusiness: 'ビジネスが制限対象の事業一覧に含まれていないことを確認してください', routingNumber: '有効なルーティング番号を入力してください', accountNumber: '有効な口座番号を入力してください', + swiftCodeOrIban: '有効なSwiftコード/IBANを入力してください', routingAndAccountNumberCannotBeSame: 'ルーティング番号と口座番号を同じにすることはできません', companyType: '有効な会社の種類を選択してください', tooManyAttempts: 'ログイン試行回数が多すぎるため、このオプションは24時間無効になっています。時間をおいてから再度お試しいただくか、代わりに詳細情報を手動で入力してください。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 983cfb709e44..7de9e185ddeb 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3623,6 +3623,9 @@ ${amount} voor ${merchant} - ${date}`, thisBankAccount: 'Deze bankrekening wordt gebruikt voor zakelijke betalingen in je workspace', accountNumber: 'Rekeningnummer', routingNumber: 'Routingnummer', + internationalBankAccountDetails: 'Internationale bankrekeninggegevens', + iban: 'IBAN', + swiftBicCode: 'SWIFT/BIC-code', chooseAnAccountBelow: 'Kies een account hieronder', addBankAccount: 'Bankrekening toevoegen', chooseAnAccount: 'Kies een account', @@ -3672,6 +3675,7 @@ ${amount} voor ${merchant} - ${date}`, restrictedBusiness: 'Bevestig dat het bedrijf niet op de lijst met beperkte bedrijven staat', routingNumber: 'Voer een geldig routingnummer in', accountNumber: 'Voer een geldig rekeningnummer in', + swiftCodeOrIban: 'Voer een geldige Swift-code/IBAN in', routingAndAccountNumberCannotBeSame: 'Routing- en rekeningnummers mogen niet hetzelfde zijn', companyType: 'Selecteer een geldig bedrijfstype', tooManyAttempts: 'Vanwege een groot aantal inlogpogingen is deze optie voor 24 uur uitgeschakeld. Probeer het later opnieuw of voer de gegevens in plaats daarvan handmatig in.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8ba7155c1df9..ec12c9b3686d 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3608,6 +3608,9 @@ ${amount} dla ${merchant} - ${date}`, thisBankAccount: 'To konto bankowe będzie używane do płatności firmowych w Twoim obszarze roboczym', accountNumber: 'Numer konta', routingNumber: 'Numer rozliczeniowy', + internationalBankAccountDetails: 'Dane międzynarodowego konta bankowego', + iban: 'IBAN', + swiftBicCode: 'Kod SWIFT/BIC', chooseAnAccountBelow: 'Wybierz konto poniżej', addBankAccount: 'Dodaj konto bankowe', chooseAnAccount: 'Wybierz konto', @@ -3657,6 +3660,7 @@ ${amount} dla ${merchant} - ${date}`, restrictedBusiness: 'Potwierdź, że firma nie znajduje się na liście działalności objętych ograniczeniami', routingNumber: 'Wprowadź prawidłowy numer rozliczeniowy', accountNumber: 'Wprowadź prawidłowy numer konta', + swiftCodeOrIban: 'Wprowadź prawidłowy kod Swift/IBAN', routingAndAccountNumberCannotBeSame: 'Numer rozliczeniowy i numer konta nie mogą być takie same', companyType: 'Wybierz prawidłowy typ firmy', tooManyAttempts: 'Ze względu na dużą liczbę prób logowania ta opcja została wyłączona na 24 godziny. Spróbuj ponownie później lub wprowadź dane ręcznie.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 6e1b8d8a3984..12d08613e967 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3612,6 +3612,9 @@ ${amount} para ${merchant} - ${date}`, thisBankAccount: 'Esta conta bancária será usada para pagamentos empresariais no seu workspace', accountNumber: 'Número da conta', routingNumber: 'Número de roteamento', + internationalBankAccountDetails: 'Dados da conta bancária internacional', + iban: 'IBAN', + swiftBicCode: 'Código SWIFT/BIC', chooseAnAccountBelow: 'Escolha uma conta abaixo', addBankAccount: 'Adicionar conta bancária', chooseAnAccount: 'Escolha uma conta', @@ -3661,6 +3664,7 @@ ${amount} para ${merchant} - ${date}`, restrictedBusiness: 'Confirme que a empresa não está na lista de empresas restritas', routingNumber: 'Insira um número de roteamento válido', accountNumber: 'Insira um número de conta válido', + swiftCodeOrIban: 'Por favor, insira um código Swift/IBAN válido', routingAndAccountNumberCannotBeSame: 'Os números de roteamento e da conta não podem ser iguais', companyType: 'Selecione um tipo de empresa válido', tooManyAttempts: 'Devido ao alto número de tentativas de login, esta opção foi desativada por 24 horas. Tente novamente mais tarde ou insira os dados manualmente.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 7ad0e99a0c82..776d0716e23c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3521,6 +3521,9 @@ ${amount},商户:${merchant} - 日期:${date}`, thisBankAccount: '此银行账户将用于您工作区的业务付款', accountNumber: '账号编号', routingNumber: '路由号码', + internationalBankAccountDetails: '国际银行账户信息', + iban: 'IBAN', + swiftBicCode: 'SWIFT/BIC 代码', chooseAnAccountBelow: '在下方选择一个账户', addBankAccount: '添加银行账户', chooseAnAccount: '选择一个账户', @@ -3569,6 +3572,7 @@ ${amount},商户:${merchant} - 日期:${date}`, restrictedBusiness: '请确认该企业不在受限制业务列表中', routingNumber: '请输入有效的路由号码', accountNumber: '请输入有效的账户号码', + swiftCodeOrIban: '请输入有效的 Swift 代码/IBAN', routingAndAccountNumberCannotBeSame: '路由号码和账号不能相同', companyType: '请选择一个有效的公司类型', tooManyAttempts: '由于登录尝试次数过多,此选项已被禁用 24 小时。请稍后重试,或改为手动输入详细信息。', diff --git a/src/libs/API/parameters/AddPersonalBankAccountParams.ts b/src/libs/API/parameters/AddPersonalBankAccountParams.ts index 496c9a6b02c6..65c6847ffc76 100644 --- a/src/libs/API/parameters/AddPersonalBankAccountParams.ts +++ b/src/libs/API/parameters/AddPersonalBankAccountParams.ts @@ -18,6 +18,8 @@ type AddPersonalBankAccountParams = { addressZip?: string; addressCountry?: string; confirmedOwnershipDetails?: boolean; + iban?: string; + swiftCode?: string; }; export default AddPersonalBankAccountParams; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 82f738b34cf8..3ff824a530cd 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -509,6 +509,8 @@ function addPersonalBankAccount( addressZip: account?.addressZipCode, addressCountry: account?.country, confirmedOwnershipDetails: account?.confirmedOwnershipDetails, + iban: account?.iban, + swiftCode: account?.swiftCode, }; if (policyID) { parameters.policyID = policyID; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx index 24ce1972a192..345b73d264a3 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx @@ -4,6 +4,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useAndroidBackButtonHandler from '@hooks/useAndroidBackButtonHandler'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useRootNavigationState from '@hooks/useRootNavigationState'; import useSubPage from '@hooks/useSubPage'; @@ -22,14 +23,15 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import {createShouldCollectInternationalDepositDetailsSelector} from '@src/selectors/Policy'; import type {InternationalBankAccountForm} from '@src/types/form'; -import type {BankAccountList, CorpayFields, PrivatePersonalDetails} from '@src/types/onyx'; +import type {BankAccountList, CorpayFields, Policy, PrivatePersonalDetails} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useRoute} from '@react-navigation/native'; -import React from 'react'; +import React, {useCallback} from 'react'; import type CustomSubPageProps from './types'; @@ -39,6 +41,7 @@ import BankAccountDetails from './subPages/BankAccountDetails'; import BankInformation from './subPages/BankInformation'; import Confirmation from './subPages/Confirmation'; import CountrySelection from './subPages/CountrySelection'; +import InternationalBankAccountDetails from './subPages/InternationalBankAccountDetails'; import Success from './subPages/Success'; import {getFieldsMap, getInitialPersonalDetailsValues, getInitialSubstep, getSubstepValues, testValidation} from './utils'; @@ -55,6 +58,7 @@ type InternationalDepositAccountContentProps = { const pages = [ {pageName: CONST.CORPAY_FIELDS.PAGE_NAME.COUNTRY, component: CountrySelection}, {pageName: CONST.CORPAY_FIELDS.PAGE_NAME.ACCOUNT_DETAILS, component: BankAccountDetails}, + {pageName: CONST.CORPAY_FIELDS.PAGE_NAME.INTERNATIONAL_BANK_ACCOUNT_DETAILS, component: InternationalBankAccountDetails}, {pageName: CONST.CORPAY_FIELDS.PAGE_NAME.ACCOUNT_TYPE, component: AccountType}, {pageName: CONST.CORPAY_FIELDS.PAGE_NAME.BANK_INFORMATION, component: BankInformation}, {pageName: CONST.CORPAY_FIELDS.PAGE_NAME.ACCOUNT_HOLDER_DETAILS, component: AccountHolderInformation}, @@ -62,8 +66,11 @@ const pages = [ {pageName: CONST.CORPAY_FIELDS.PAGE_NAME.SUCCESS, component: Success}, ]; -function getSkippedPages(skipAccountTypeStep: boolean, skipAccountHolderInformationStep: boolean) { +function getSkippedPages(skipAccountTypeStep: boolean, skipAccountHolderInformationStep: boolean, skipInternationalBankAccountDetailsStep: boolean) { const skippedSteps = []; + if (skipInternationalBankAccountDetailsStep) { + skippedSteps.push(CONST.CORPAY_FIELDS.PAGE_NAME.INTERNATIONAL_BANK_ACCOUNT_DETAILS); + } if (skipAccountTypeStep) { skippedSteps.push(CONST.CORPAY_FIELDS.PAGE_NAME.ACCOUNT_TYPE); } @@ -87,6 +94,12 @@ function InternationalDepositAccountContent({ const fieldsMap = getFieldsMap(corpayFields); const values = getSubstepValues(privatePersonalDetails, corpayFields, bankAccountList, draftValues, country, fieldsMap); + const bankCountry = values.bankCountry; + const shouldCollectInternationalDepositDetailsSelector = useCallback( + (policies: OnyxCollection) => createShouldCollectInternationalDepositDetailsSelector(bankCountry)(policies), + [bankCountry], + ); + const [shouldCollectInternationalDepositDetails] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: shouldCollectInternationalDepositDetailsSelector}); const initialAccountHolderDetailsValues = getInitialPersonalDetailsValues(privatePersonalDetails); @@ -96,7 +109,11 @@ function InternationalDepositAccountContent({ const skipAccountHolderInformationStep = testValidation(initialAccountHolderDetailsValues, fieldsMap[CONST.CORPAY_FIELDS.PAGE_NAME.ACCOUNT_HOLDER_DETAILS]); - const skippedPages = getSkippedPages(skipAccountTypeStep, skipAccountHolderInformationStep); + // Skip the international bank account details step when no collecting policy needs international details for the + // selected bank country, or once we already have both the IBAN and SWIFT/BIC code. + const skipInternationalBankAccountDetailsStep = !shouldCollectInternationalDepositDetails || (!!values.iban && !!values.swiftCode); + + const skippedPages = getSkippedPages(skipAccountTypeStep, skipAccountHolderInformationStep, skipInternationalBankAccountDetailsStep); const route = useRoute>(); const topmostFullScreenRoute = useRootNavigationState((state) => state?.routes.findLast((r) => isFullScreenName(r.name))); diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx index 34f3e81e2c17..82bbf9ba65c5 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx @@ -14,11 +14,16 @@ import {addPersonalBankAccount} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {createShouldCollectInternationalDepositDetailsSelector} from '@src/selectors/Policy'; +import type {Policy} from '@src/types/onyx'; -import React, {useEffect, useRef} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; + +import React, {useCallback, useEffect, useRef} from 'react'; import Address from './substeps/AddressStep'; import Confirmation from './substeps/ConfirmationStep'; +import InternationalBankAccountDetails from './substeps/InternationalBankAccountDetailsStep'; import LegalName from './substeps/LegalNameStep'; import ManualBankAccountDetails from './substeps/ManualBankAccountDetailsStep'; import PhoneNumber from './substeps/PhoneNumberStep'; @@ -26,8 +31,8 @@ import PlaidBankAccount from './substeps/PlaidBankAccountStep'; import getSkippedStepsPersonalInfo from './utils/getSkippedStepsPersonalInfo'; const bodyContentInfoSet: Array> = [LegalName, Address, PhoneNumber, Confirmation]; -const bodyContentWithPlaid: Array> = [PlaidBankAccount, ...bodyContentInfoSet]; -const bodyContentWithManualSetup: Array> = [ManualBankAccountDetails, ...bodyContentInfoSet]; +const bodyContentWithPlaid: Array> = [PlaidBankAccount, InternationalBankAccountDetails, ...bodyContentInfoSet]; +const bodyContentWithManualSetup: Array> = [ManualBankAccountDetails, InternationalBankAccountDetails, ...bodyContentInfoSet]; const DEFAULT_OBJECT = {}; const ACCOUNT_OWNERSHIP_ERROR_SUBSTRING = 'account ownership'; @@ -45,6 +50,11 @@ function PersonalInfoPage() { const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA); + const shouldCollectInternationalDepositDetailsSelector = useCallback( + (policies: OnyxCollection) => createShouldCollectInternationalDepositDetailsSelector(CONST.COUNTRY.US)(policies), + [], + ); + const [shouldCollectInternationalDepositDetails] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: shouldCollectInternationalDepositDetailsSelector}); const submitBankAccountForm = () => { const bankAccounts = plaidData?.bankAccounts ?? []; @@ -69,7 +79,10 @@ function PersonalInfoPage() { addPersonalBankAccount(accountData, personalPolicyID); }; - const skipSteps = getSkippedStepsPersonalInfo(privatePersonalDetails); + const isAccountNumberIBAN = CONST.BANK_ACCOUNT.REGEX.IBAN.test((personalBankAccount?.accountNumber ?? '').trim()); + const shouldSkipInternationalBankAccountDetails = !shouldCollectInternationalDepositDetails || (isAccountNumberIBAN && !!personalBankAccount?.swiftCode); + + const skipSteps = getSkippedStepsPersonalInfo(privatePersonalDetails, shouldSkipInternationalBankAccountDetails); const { componentToRender: SubStep, diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/InternationalBankAccountDetailsStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/InternationalBankAccountDetailsStep.tsx new file mode 100644 index 000000000000..581aed8ca6f7 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/InternationalBankAccountDetailsStep.tsx @@ -0,0 +1,88 @@ +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; + +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePersonalBankAccountDetailsFormSubmit from '@hooks/usePersonalBankAccountDetailsFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {getFieldRequiredErrors} from '@libs/ValidationUtils'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; + +import React from 'react'; + +type InternationalBankAccountDetailsProps = SubStepProps; + +const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; +const STEP_FIELDS = [BANK_INFO_STEP_KEYS.IBAN, BANK_INFO_STEP_KEYS.SWIFT_CODE]; + +function InternationalBankAccountDetailsStep({onNext, isEditing}: InternationalBankAccountDetailsProps) { + const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {inputCallbackRef} = useAutoFocusInput(); + + const validate = (values: FormOnyxValues): FormInputErrors => { + const errors = getFieldRequiredErrors(values, STEP_FIELDS, translate); + + if (values.iban && !CONST.BANK_ACCOUNT.REGEX.IBAN.test(values.iban.trim())) { + errors.iban = translate('bankAccount.error.swiftCodeOrIban'); + } + if (values.swiftCode && !CONST.BANK_ACCOUNT.REGEX.INTERNATIONAL_SWIFT_CODE.test(values.swiftCode.trim())) { + errors.swiftCode = translate('bankAccount.error.swiftCodeOrIban'); + } + + return errors; + }; + + const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: true, + }); + + return ( + + {translate('bankAccount.internationalBankAccountDetails')} + + + + ); +} + +InternationalBankAccountDetailsStep.displayName = 'InternationalBankAccountDetailsStep'; + +export default InternationalBankAccountDetailsStep; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts index 2b56438f356d..12c8715e1fed 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts @@ -4,24 +4,32 @@ import CONST from '@src/CONST'; import type {PrivatePersonalDetails} from '@src/types/onyx'; /** - * Returns the initial substep for the Personal Info step based on already existing data + * Returns the steps to skip in the Personal Info flow based on already existing data. + * + * The international bank account details step sits at index 1 (right after the bank account details step), + * so the legal name / address / phone number steps that follow it are at indexes 2, 3 and 4. */ -function getSkippedStepsPersonalInfo(data?: Partial): number[] { +function getSkippedStepsPersonalInfo(data: Partial | undefined, shouldSkipInternationalBankAccountDetails: boolean): number[] { const currentAddress = getCurrentAddress(data); const skippedSteps = []; - if (!!data?.legalFirstName && !!data?.legalLastName) { + + if (shouldSkipInternationalBankAccountDetails) { skippedSteps.push(1); } + if (!!data?.legalFirstName && !!data?.legalLastName) { + skippedSteps.push(2); + } + const isUsOrCanada = currentAddress?.country === CONST.COUNTRY.US || currentAddress?.country === CONST.COUNTRY.CA; const hasValidState = !isUsOrCanada || !!currentAddress?.state; if (!!currentAddress?.street && !!currentAddress?.city && hasValidState && !!currentAddress?.zip) { - skippedSteps.push(2); + skippedSteps.push(3); } if (data?.phoneNumber) { - skippedSteps.push(3); + skippedSteps.push(4); } return skippedSteps; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/subPages/InternationalBankAccountDetails.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/subPages/InternationalBankAccountDetails.tsx new file mode 100644 index 000000000000..c02b0898efb5 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/subPages/InternationalBankAccountDetails.tsx @@ -0,0 +1,87 @@ +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; + +import useInternationalBankAccountFormSubmit from '@hooks/useInternationalBankAccountFormSubmit'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {getFieldRequiredErrors} from '@libs/ValidationUtils'; + +import type CustomSubPageProps from '@pages/settings/Wallet/InternationalDepositAccount/types'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +import React, {useCallback} from 'react'; +import {View} from 'react-native'; + +const IBAN = 'iban'; +const SWIFT_CODE = 'swiftCode'; +const STEP_FIELDS = [IBAN, SWIFT_CODE]; + +function InternationalBankAccountDetails({isEditing, onNext, formValues}: CustomSubPageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const handleSubmit = useInternationalBankAccountFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: true, + }); + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = getFieldRequiredErrors(values, STEP_FIELDS, translate); + + if (values[IBAN] && !CONST.BANK_ACCOUNT.REGEX.IBAN.test(String(values[IBAN]).trim())) { + errors[IBAN] = translate('bankAccount.error.swiftCodeOrIban'); + } + if (values[SWIFT_CODE] && !CONST.BANK_ACCOUNT.REGEX.INTERNATIONAL_SWIFT_CODE.test(String(values[SWIFT_CODE]).trim())) { + errors[SWIFT_CODE] = translate('bankAccount.error.swiftCodeOrIban'); + } + + return errors; + }, + [translate], + ); + + return ( + + + {translate('bankAccount.internationalBankAccountDetails')} + + + + + ); +} + +export default InternationalBankAccountDetails; diff --git a/src/selectors/Policy.ts b/src/selectors/Policy.ts index 3a15511e36b6..f6d932dafe5a 100644 --- a/src/selectors/Policy.ts +++ b/src/selectors/Policy.ts @@ -256,6 +256,18 @@ const createFilteredPoliciesInfoSelector = return {filteredPoliciesCount, firstPolicyID}; }; +/** + * Creates a selector returning whether international deposit details (IBAN/SWIFT) should be collected for the given + * bank country. + */ +const createShouldCollectInternationalDepositDetailsSelector = + (bankCountry: string) => + (policies: OnyxCollection): boolean => + Object.values(policies ?? {}).some((policy) => { + const countries = Object.keys(policy?.reimbursement?.countries ?? {}); + return countries.length > 0 && !countries.includes(bankCountry); + }); + const hasOnlyPersonalPoliciesSelector = (policies: OnyxCollection): boolean => { return !Object.values(policies ?? {}).some((policy) => policy && policy.type !== CONST.POLICY.TYPE.PERSONAL && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); }; @@ -385,6 +397,7 @@ export { hasReusablePoliciesConnectedToSelector, lastWorkspaceNumberSelector, hasOnlyPersonalPoliciesSelector, + createShouldCollectInternationalDepositDetailsSelector, policyNameSelector, policyTypeSelector, areInvoicesEnabledSelector, diff --git a/src/types/form/PersonalBankAccountForm.ts b/src/types/form/PersonalBankAccountForm.ts index 126d61cf2ff0..4f2817ef2ab3 100644 --- a/src/types/form/PersonalBankAccountForm.ts +++ b/src/types/form/PersonalBankAccountForm.ts @@ -22,6 +22,8 @@ const INPUT_IDS = { COUNTRY: 'country', ZIP_CODE: 'addressZipCode', PHONE_NUMBER: 'phoneNumber', + IBAN: 'iban', + SWIFT_CODE: 'swiftCode', }, } as const; @@ -42,6 +44,8 @@ type BankAccountStepProps = { [INPUT_IDS.BANK_INFO_STEP.ZIP_CODE]: string; [INPUT_IDS.BANK_INFO_STEP.COUNTRY]: string; [INPUT_IDS.BANK_INFO_STEP.PHONE_NUMBER]: string; + [INPUT_IDS.BANK_INFO_STEP.IBAN]: string; + [INPUT_IDS.BANK_INFO_STEP.SWIFT_CODE]: string; }; type PlaidAccountProps = { diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index e5b538ecea21..3ddaa916056a 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -2505,6 +2505,15 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** The reimbursement choice for policy */ reimbursementChoice?: ValueOf; + /** Configuration for collecting employee deposit account details for reimbursement outside of Expensify */ + reimbursement?: { + /** Whether reimbursement is enabled for the policy */ + enabled?: boolean; + + /** Countries (keyed by ISO code) where the company has a withdrawal account it can reimburse from */ + countries?: Record; + }; + /** The set reimburser for the policy */ reimburser?: string;