From 04a4154be97605e90f10d5b0652c4fcb8146be47 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 21:42:33 +0300 Subject: [PATCH 01/63] feat: Update option missing for Personal Bank Accounts in ND compared to Classic --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + src/languages/de.ts | 2 + src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/languages/fr.ts | 2 + src/languages/it.ts | 2 + src/languages/ja.ts | 2 + src/languages/nl.ts | 2 + src/languages/pl.ts | 2 + src/languages/pt-BR.ts | 2 + src/languages/zh-hans.ts | 2 + .../UpdatePersonalBankAccountInfoParams.ts | 12 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/BankAccountUtils.ts | 37 ++++++- .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 4 + src/libs/Navigation/types.ts | 1 + src/libs/actions/BankAccounts.ts | 81 ++++++++++++++ .../settings/Wallet/PaymentMethodList.tsx | 8 ++ .../settings/Wallet/PaymentMethodListItem.tsx | 18 +++- .../Wallet/UpdatePersonalBankAccountPage.tsx | 85 +++++++++++++++ .../Wallet/UpdatePersonalInfoConfirmation.tsx | 101 ++++++++++++++++++ .../settings/Wallet/WalletPage/index.tsx | 8 ++ 25 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts create mode 100644 src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx create mode 100644 src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 87990bbe91f86..c74834bb9ccf9 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -359,6 +359,7 @@ const ROUTES = { SETTINGS_ADD_US_BANK_ACCOUNT: 'settings/wallet/add-us-bank-account', SETTINGS_ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'settings/wallet/add-us-bank-account/entry-point', + SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT: 'settings/wallet/update-personal-bank-account', SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: `settings/wallet/add-bank-account/select-country/${VERIFY_ACCOUNT}`, SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', SETTINGS_WALLET_UNSHARE_BANK_ACCOUNT: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9744c56eac725..2ccfb0905fc84 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -107,6 +107,7 @@ const SCREENS = { ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', ADD_US_BANK_ACCOUNT: 'Settings_Add_US_Bank_Account', ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'Settings_Add_US_Bank_Account_Entry_Point', + UPDATE_PERSONAL_BANK_ACCOUNT: 'Settings_Update_Personal_Bank_Account', ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: 'Settings_Add_Bank_Account_Select_Country_Verify_Account', CLOSE: 'Settings_Close', REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged', diff --git a/src/languages/de.ts b/src/languages/de.ts index a8aea5eac5b00..0a9aaf12e4e58 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3108,6 +3108,8 @@ ${ confirmationStepHeader: 'Überprüfen Sie Ihre Angaben.', confirmationStepSubHeader: 'Überprüfen Sie die untenstehenden Angaben und aktivieren Sie das Kontrollkästchen für die Bedingungen, um zu bestätigen.', toGetStarted: 'Fügen Sie ein persönliches Bankkonto hinzu, um Erstattungen zu erhalten, Rechnungen zu bezahlen oder die Expensify Wallet zu aktivieren.', + updatePersonalInfo: 'Bankkonto aktualisieren', + updatePersonalInfoFailure: 'Die Bankkontoinformationen konnten nicht aktualisiert werden. Bitte versuchen Sie es später erneut.', }, addPersonalBankAccountPage: { enterPassword: 'Expensify-Passwort eingeben', diff --git a/src/languages/en.ts b/src/languages/en.ts index 3901ba16f134c..bf5927b13857f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3092,6 +3092,8 @@ const translations = { confirmationStepHeader: 'Check your info.', confirmationStepSubHeader: 'Double check the details below, and check the terms box to confirm.', toGetStarted: 'Add a personal bank account to receive reimbursements, pay invoices, or enable the Expensify Wallet.', + updatePersonalInfo: 'Update bank account', + updatePersonalInfoFailure: 'Unable to update bank account information. Please try again later.', }, addPersonalBankAccountPage: { enterPassword: 'Enter Expensify password', diff --git a/src/languages/es.ts b/src/languages/es.ts index b8b2a20520101..e605fc5fad7c1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2826,6 +2826,8 @@ ${amount} para ${merchant} - ${date}`, confirmationStepHeader: 'Verifica tu información.', confirmationStepSubHeader: 'Verifica dos veces los detalles a continuación y marca la casilla de términos para confirmar.', toGetStarted: 'Agrega una cuenta bancaria personal para recibir reembolsos, pagar facturas o habilitar la Cartera de Expensify.', + updatePersonalInfo: 'Actualizar cuenta bancaria', + updatePersonalInfoFailure: 'No se pudo actualizar la información de la cuenta bancaria. Por favor, inténtalo de nuevo más tarde.', }, addPersonalBankAccountPage: { enterPassword: 'Escribe tu contraseña de Expensify', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index af84cd2f1c92b..21acf116819fc 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3115,6 +3115,8 @@ ${ confirmationStepHeader: 'Vérifiez vos informations.', confirmationStepSubHeader: 'Vérifiez les détails ci-dessous, puis cochez la case des conditions pour confirmer.', toGetStarted: 'Ajoutez un compte bancaire personnel pour recevoir des remboursements, payer des factures ou activer le portefeuille Expensify.', + updatePersonalInfo: 'Mettre à jour le compte bancaire', + updatePersonalInfoFailure: 'Impossible de mettre à jour les informations du compte bancaire. Veuillez réessayer plus tard.', }, addPersonalBankAccountPage: { enterPassword: 'Saisissez le mot de passe Expensify', diff --git a/src/languages/it.ts b/src/languages/it.ts index b3520540ccca0..94166138c631c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3099,6 +3099,8 @@ ${ confirmationStepHeader: 'Controlla le tue informazioni.', confirmationStepSubHeader: 'Controlla attentamente i dettagli qui sotto e seleziona la casella delle condizioni per confermare.', toGetStarted: 'Aggiungi un conto bancario personale per ricevere rimborsi, pagare fatture o abilitare il portafoglio Expensify.', + updatePersonalInfo: 'Aggiorna conto bancario', + updatePersonalInfoFailure: 'Impossibile aggiornare le informazioni del conto bancario. Riprova più tardi.', }, addPersonalBankAccountPage: { enterPassword: 'Inserisci la password di Expensify', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 3b94aedff1d88..84dbe276551be 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3089,6 +3089,8 @@ ${ confirmationStepHeader: '情報を確認してください。', confirmationStepSubHeader: '以下の詳細を再確認し、確認するには利用規約のチェックボックスをオンにしてください。', toGetStarted: '払い戻しを受け取ったり、請求書を支払ったり、Expensify Wallet を有効にしたりするには、個人の銀行口座を追加します。', + updatePersonalInfo: '銀行口座を更新', + updatePersonalInfoFailure: '銀行口座情報を更新できませんでした。後でもう一度お試しください。', }, addPersonalBankAccountPage: { enterPassword: 'Expensify のパスワードを入力', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 75d460f2c5c30..cc6af81592e41 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3097,6 +3097,8 @@ ${ confirmationStepHeader: 'Controleer je gegevens.', confirmationStepSubHeader: 'Controleer de onderstaande gegevens en vink het vakje met de voorwaarden aan om te bevestigen.', toGetStarted: 'Voeg een persoonlijke bankrekening toe om vergoedingen te ontvangen, facturen te betalen of de Expensify Wallet in te schakelen.', + updatePersonalInfo: 'Bankrekening bijwerken', + updatePersonalInfoFailure: 'Kan de bankrekeninggegevens niet bijwerken. Probeer het later opnieuw.', }, addPersonalBankAccountPage: { enterPassword: 'Expensify-wachtwoord invoeren', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 7d54b2fedb5fa..0421f6d2c1db1 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3091,6 +3091,8 @@ ${ confirmationStepHeader: 'Sprawdź swoje dane.', confirmationStepSubHeader: 'Sprawdź poniższe szczegóły i zaznacz pole z warunkami, aby potwierdzić.', toGetStarted: 'Dodaj osobiste konto bankowe, aby otrzymywać zwroty kosztów, opłacać faktury lub włączyć portfel Expensify.', + updatePersonalInfo: 'Aktualizuj konto bankowe', + updatePersonalInfoFailure: 'Nie można zaktualizować informacji o koncie bankowym. Spróbuj ponownie później.', }, addPersonalBankAccountPage: { enterPassword: 'Wprowadź hasło do Expensify', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 6458784e8ff4e..bad0f12662c2d 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3091,6 +3091,8 @@ ${ confirmationStepHeader: 'Verifique suas informações.', confirmationStepSubHeader: 'Verifique os detalhes abaixo e marque a caixa de termos para confirmar.', toGetStarted: 'Adicione uma conta bancária pessoal para receber reembolsos, pagar faturas ou ativar a Carteira Expensify.', + updatePersonalInfo: 'Atualizar conta bancária', + updatePersonalInfoFailure: 'Não foi possível atualizar as informações da conta bancária. Por favor, tente novamente mais tarde.', }, addPersonalBankAccountPage: { enterPassword: 'Digite a senha do Expensify', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index df99a8ac8b215..4b8d69bde8015 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3048,6 +3048,8 @@ ${ confirmationStepHeader: '检查您的信息。', confirmationStepSubHeader: '请仔细核对以下详情,并勾选条款复选框以确认。', toGetStarted: '添加个人银行账户以接收报销、支付发票或启用 Expensify 钱包。', + updatePersonalInfo: '更新银行账户', + updatePersonalInfoFailure: '无法更新银行账户信息。请稍后重试。', }, addPersonalBankAccountPage: { enterPassword: '输入 Expensify 密码', diff --git a/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts b/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts new file mode 100644 index 0000000000000..afb3c7c2d3c5a --- /dev/null +++ b/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts @@ -0,0 +1,12 @@ +type UpdatePersonalBankAccountInfoParams = { + phoneNumber?: string; + legalFirstName?: string; + legalLastName?: string; + addressStreet?: string; + addressCity?: string; + addressState?: string; + addressZip?: string; + addressCountry?: string; +}; + +export default UpdatePersonalBankAccountInfoParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index b65ee965c5255..6480d79b587da 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -144,6 +144,7 @@ export type {default as FlagCommentParams} from './FlagCommentParams'; export type {default as UpdateReportPrivateNoteParams} from './UpdateReportPrivateNoteParams'; export type {default as UpdateCompanyInformationForBankAccountParams} from './UpdateCompanyInformationForBankAccountParams'; export type {default as UpdatePersonalDetailsForWalletParams} from './UpdatePersonalDetailsForWalletParams'; +export type {default as UpdatePersonalBankAccountInfoParams} from './UpdatePersonalBankAccountInfoParams'; export type {default as VerifyIdentityParams} from './VerifyIdentityParams'; export type {default as AcceptWalletTermsParams} from './AcceptWalletTermsParams'; export type {default as ChronosRemoveOOOEventParams} from './ChronosRemoveOOOEventParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index fc338163b81fc..792f159f74be7 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -110,6 +110,7 @@ const WRITE_COMMANDS = { ADD_TEXT_AND_ATTACHMENT: 'AddTextAndAttachment', CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid', ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', + UPDATE_PERSONAL_BANK_ACCOUNT_INFO: 'UpdatePersonalBankAccountInfo', RESTART_BANK_ACCOUNT_SETUP: 'RestartBankAccountSetup', RESEND_VALIDATE_CODE: 'ResendValidateCode', READ_NEWEST_ACTION: 'ReadNewestAction', @@ -633,6 +634,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]: Parameters.AddCommentOrAttachmentParams; [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: Parameters.ConnectBankAccountParams; [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams; + [WRITE_COMMANDS.UPDATE_PERSONAL_BANK_ACCOUNT_INFO]: Parameters.UpdatePersonalBankAccountInfoParams; [WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP]: Parameters.RestartBankAccountSetupParams; [WRITE_COMMANDS.RESEND_VALIDATE_CODE]: null; [WRITE_COMMANDS.READ_NEWEST_ACTION]: Parameters.ReadNewestActionParams; diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index e3871c589c2f5..8b46468c8175e 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -2,6 +2,8 @@ import {Str} from 'expensify-common'; import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type * as OnyxTypes from '@src/types/onyx'; +import type AccountData from '@src/types/onyx/AccountData'; +import {getCurrentAddress} from './PersonalDetailsUtils'; function getDefaultCompanyWebsite(session: OnyxEntry, account: OnyxEntry, shouldShowPublicDomain = false): string { return account?.isFromPublicDomain && !shouldShowPublicDomain ? '' : `https://www.${Str.extractEmailDomain(session?.email ?? '')}`; @@ -19,4 +21,37 @@ function hasPartiallySetupBankAccount(bankAccountList: OnyxEntry isBankAccountPartiallySetup(bankAccount?.accountData?.state)); } -export {getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, isBankAccountPartiallySetup}; +/** + * Check if a US personal bank account in OPEN state is missing required personal information + * (legal name, address, or phone number) from PrivatePersonalDetails. + * + * This is used to show "Action required" badge for existing accounts that need updates + * to enable global reimbursement payments. + */ +function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined, privatePersonalDetails: OnyxEntry): boolean { + // Only applies to personal bank accounts + if (accountData?.type !== CONST.BANK_ACCOUNT.TYPE.PERSONAL) { + return false; + } + + // Only applies to fully setup accounts (OPEN state) + // Partially setup accounts already show "Action required" via isAccountInSetupState + if (accountData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN) { + return false; + } + + // Only applies to US accounts + if (accountData?.additionalData?.country !== CONST.COUNTRY.US) { + return false; + } + + // Check if personal details are missing + const currentAddress = getCurrentAddress(privatePersonalDetails); + const hasLegalName = !!privatePersonalDetails?.legalFirstName && !!privatePersonalDetails?.legalLastName; + const hasAddress = !!currentAddress?.street && !!currentAddress?.city && !!currentAddress?.state && !!currentAddress?.zip; + const hasPhoneNumber = !!privatePersonalDetails?.phoneNumber; + + return !hasLegalName || !hasAddress || !hasPhoneNumber; +} + +export {getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, isBankAccountPartiallySetup, isPersonalBankAccountMissingInfo}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 7bdfeca882ef5..34604d5e5dfd5 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -401,6 +401,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/AddPersonalBankAccountPage').default, [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT_ENTRY_POINT]: () => require('../../../../pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint').default, + [SCREENS.SETTINGS.UPDATE_PERSONAL_BANK_ACCOUNT]: () => require('../../../../pages/settings/Wallet/UpdatePersonalBankAccountPage').default, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Wallet/InternationalDepositAccount/CountrySelectionVerifyAccountPage').default, [SCREENS.SETTINGS.RULES.ROOT]: () => require('../../../../pages/settings/Rules/ExpenseRulesPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 2b8ac2cef4ec6..3bdf763232d34 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -308,6 +308,10 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT_ENTRY_POINT, exact: true, }, + [SCREENS.SETTINGS.UPDATE_PERSONAL_BANK_ACCOUNT]: { + path: ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT, + exact: true, + }, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: { path: ROUTES.SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3a6f8cec8c1e9..4f8178a6f4b24 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -222,6 +222,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: undefined; [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT_ENTRY_POINT]: undefined; + [SCREENS.SETTINGS.UPDATE_PERSONAL_BANK_ACCOUNT]: undefined; [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER]: undefined; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 2875cdbc4391b..81dbe7fe1e1f4 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -147,6 +147,86 @@ function clearPersonalBankAccountErrors() { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); } +/** + * Updates personal information (name, address, phone) for a personal bank account. + * This is used when a US personal bank account in OPEN state is missing required info. + */ +function updatePersonalBankAccountInfo(accountData: Partial) { + const parameters = { + phoneNumber: accountData?.phoneNumber, + legalFirstName: accountData?.legalFirstName, + legalLastName: accountData?.legalLastName, + addressStreet: getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2), + addressCity: accountData?.addressCity, + addressState: accountData?.addressState, + addressZip: accountData?.addressZipCode, + addressCountry: accountData?.country, + }; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, + value: { + isLoading: true, + errors: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, + value: { + isLoading: false, + errors: null, + shouldShowSuccess: true, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + legalFirstName: accountData?.legalFirstName, + legalLastName: accountData?.legalLastName, + phoneNumber: accountData?.phoneNumber, + addresses: accountData?.addressStreet + ? [ + { + street: getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2), + city: accountData?.addressCity, + state: accountData?.addressState, + zip: accountData?.addressZipCode, + country: accountData?.country as Country, + current: true, + }, + ] + : undefined, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, + value: { + isLoading: false, + errors: getMicroSecondOnyxErrorWithTranslationKey('addPersonalBankAccount.updatePersonalInfoFailure'), + }, + }, + ], + }; + + API.write(WRITE_COMMANDS.UPDATE_PERSONAL_BANK_ACCOUNT_INFO, parameters, onyxData); + + // Clear the form draft after submission + Onyx.set(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, null); + + // Navigate back to wallet page + Navigation.goBack(ROUTES.SETTINGS_WALLET); +} + /** * Whether after adding a bank account we should continue with the KYC flow. If so, we must specify the fallback route. */ @@ -1504,4 +1584,5 @@ export { getBankAccountFromID, openBankAccountSharePage, clearShareBankAccountErrors, + updatePersonalBankAccountInfo, }; diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index b85b203c39458..9d4e74a5e06f0 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -19,6 +19,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; +import {isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import { filterPersonalCards, getAssignedCardSortKey, @@ -352,6 +353,10 @@ function PaymentMethodList({ methodID: paymentMethod.methodID, description: paymentMethod.description, }; + + // Check if this is a personal bank account missing required personal info + const isMissingPersonalInfo = isPersonalBankAccountMissingInfo(paymentMethod.accountData, privatePersonalDetails); + return { ...paymentMethod, title: paymentMethod.title?.includes(CONST.MASKED_PAN_PREFIX) ? paymentMethod.accountData?.additionalData?.bankName : paymentMethod.title, @@ -372,6 +377,8 @@ function PaymentMethodList({ iconRight: itemIconRight ?? expensifyIcons.ThreeDots, shouldShowRightIcon, canDismissError: true, + isMissingPersonalInfo, + brickRoadIndicator: isMissingPersonalInfo ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }; }); return combinedPaymentMethods; @@ -396,6 +403,7 @@ function PaymentMethodList({ activePaymentMethodID, actionPaymentMethodType, onThreeDotsMenuPress, + privatePersonalDetails, ]); const onPressItem = useCallback(() => { diff --git a/src/pages/settings/Wallet/PaymentMethodListItem.tsx b/src/pages/settings/Wallet/PaymentMethodListItem.tsx index 701473ec5cb21..a84b55fc972a5 100644 --- a/src/pages/settings/Wallet/PaymentMethodListItem.tsx +++ b/src/pages/settings/Wallet/PaymentMethodListItem.tsx @@ -37,6 +37,8 @@ type PaymentMethodItem = PaymentMethod & { cardID?: number; plaidUrl?: string; onThreeDotsMenuPress?: (e: GestureResponderEvent | KeyboardEvent | undefined) => void; + /** Whether the personal bank account is missing required personal info (name, address, phone) */ + isMissingPersonalInfo?: boolean; } & BankIcon; type PaymentMethodListItemProps = { @@ -89,6 +91,13 @@ function isAccountInSetupState(account: PaymentMethodItem) { return !!(account.accountData && 'state' in account.accountData && isBankAccountPartiallySetup(account.accountData.state)); } +/** + * Returns true if the account needs action - either partially setup or missing personal info + */ +function isAccountNeedingAction(account: PaymentMethodItem) { + return isAccountInSetupState(account) || !!account.isMissingPersonalInfo; +} + function PaymentMethodListItem({item, shouldShowDefaultBadge, threeDotsMenuItems, listItemStyle}: PaymentMethodListItemProps) { const icons = useMemoizedLazyExpensifyIcons(['DotIndicator']); const styles = useThemeStyles(); @@ -96,7 +105,8 @@ function PaymentMethodListItem({item, shouldShowDefaultBadge, threeDotsMenuItems const threeDotsMenuRef = useRef<{hidePopoverMenu: () => void; isPopupMenuVisible: boolean; onThreeDotsPress: () => void}>(null); const handleRowPress = (e: GestureResponderEvent | KeyboardEvent | undefined) => { - if (isAccountInSetupState(item) || !threeDotsMenuItems || (item.cardID && item.onThreeDotsMenuPress)) { + // If account needs action (setup or missing info), navigate directly instead of showing menu + if (isAccountNeedingAction(item) || !threeDotsMenuItems || (item.cardID && item.onThreeDotsMenuPress)) { item.onPress?.(e); } else if (threeDotsMenuRef.current) { threeDotsMenuRef.current.onThreeDotsPress(); @@ -105,7 +115,7 @@ function PaymentMethodListItem({item, shouldShowDefaultBadge, threeDotsMenuItems const getBadgeText = useCallback( (listItem: PaymentMethodItem) => { - if (isAccountInSetupState(listItem)) { + if (isAccountNeedingAction(listItem)) { return translate('common.actionRequired'); } return shouldShowDefaultBadge ? translate('paymentMethodList.defaultPaymentMethod') : undefined; @@ -133,8 +143,8 @@ function PaymentMethodListItem({item, shouldShowDefaultBadge, threeDotsMenuItems iconWidth={item.iconWidth ?? item.iconSize} iconStyles={item.iconStyles} badgeText={getBadgeText(item)} - badgeIcon={isAccountInSetupState(item) ? icons.DotIndicator : undefined} - badgeSuccess={isAccountInSetupState(item) ? true : undefined} + badgeIcon={isAccountNeedingAction(item) ? icons.DotIndicator : undefined} + badgeSuccess={isAccountNeedingAction(item) ? true : undefined} wrapperStyle={[styles.paymentMethod, listItemStyle]} iconRight={item.iconRight} shouldShowRightIcon={!threeDotsMenuItems && item.shouldShowRightIcon} diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx new file mode 100644 index 0000000000000..1067dae090226 --- /dev/null +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useSubStep from '@hooks/useSubStep'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import {formatE164PhoneNumber} from '@libs/LoginUtils'; +import Navigation from '@navigation/Navigation'; +import {updatePersonalBankAccountInfo} from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import Address from './InternationalDepositAccount/PersonalInfo/substeps/AddressStep'; +import LegalName from './InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep'; +import PhoneNumber from './InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep'; +import getSkippedStepsPersonalInfo from './InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo'; +import UpdatePersonalInfoConfirmation from './UpdatePersonalInfoConfirmation'; + +const bodyContent: Array> = [LegalName, Address, PhoneNumber, UpdatePersonalInfoConfirmation]; + +function UpdatePersonalBankAccountPage() { + const {translate} = useLocalize(); + + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const [personalBankAccountDraft] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + + const submitPersonalInfo = () => { + const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; + const accountData = { + ...privatePersonalDetails, + ...personalBankAccountDraft, + phoneNumber: formatE164PhoneNumber(finalPhoneNumber, countryCode), + }; + updatePersonalBankAccountInfo(accountData); + }; + + // Skip steps where personal info already exists + // Step indices: 0 = LegalName, 1 = Address, 2 = PhoneNumber, 3 = Confirmation + // getSkippedStepsPersonalInfo returns indices 1, 2, 3 for original flow, but we have 0-indexed steps + const skipSteps = getSkippedStepsPersonalInfo(privatePersonalDetails); + + const { + componentToRender: SubStep, + isEditing, + nextScreen, + prevScreen, + moveTo, + screenIndex, + goToTheLastStep, + } = useSubStep({ + bodyContent, + skipSteps, + onFinished: submitPersonalInfo, + }); + + const handleBackButtonPress = () => { + if (isEditing) { + goToTheLastStep(); + return; + } + if (screenIndex === 0) { + Navigation.goBack(); + return; + } + prevScreen(); + }; + + return ( + + + + ); +} + +UpdatePersonalBankAccountPage.displayName = 'UpdatePersonalBankAccountPage'; + +export default UpdatePersonalBankAccountPage; diff --git a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx new file mode 100644 index 0000000000000..8730572f7187b --- /dev/null +++ b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import CommonConfirmationStep from '@components/SubStepForms/ConfirmationStep'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; +import {formatE164PhoneNumber} from '@libs/LoginUtils'; +import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; +import {clearPersonalBankAccountErrors} from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; + +const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; + +const DEFAULT_OBJECT = {}; + +function UpdatePersonalInfoConfirmation({onNext, onMove, isEditing}: SubStepProps) { + const {translate} = useLocalize(); + + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); + const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {canBeMissing: true}); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + + const isLoading = personalBankAccount?.isLoading ?? false; + const error = getLatestErrorMessage(personalBankAccount ?? DEFAULT_OBJECT); + + const getPersonalDetails = () => { + const currentAddress = getCurrentAddress(privatePersonalDetails); + const phone = bankAccountPersonalDetails?.phoneNumber ?? privatePersonalDetails?.phoneNumber; + return { + phoneNumber: (phone && formatE164PhoneNumber(phone, countryCode)) ?? '', + legalFirstName: bankAccountPersonalDetails?.legalFirstName ?? privatePersonalDetails?.legalFirstName ?? '', + legalLastName: bankAccountPersonalDetails?.legalLastName ?? privatePersonalDetails?.legalLastName ?? '', + addressStreet: bankAccountPersonalDetails?.addressStreet ?? currentAddress?.street ?? '', + addressCity: bankAccountPersonalDetails?.addressCity ?? currentAddress?.city ?? '', + addressState: bankAccountPersonalDetails?.addressState ?? currentAddress?.state ?? '', + addressZip: bankAccountPersonalDetails?.addressZipCode ?? currentAddress?.zip ?? '', + }; + }; + + const moveToEditStep = (step: number) => { + if (error) { + clearPersonalBankAccountErrors(); + } + onMove(step); + }; + + const getSummaryItems = () => { + const personalDetails = getPersonalDetails(); + + // Only show personal info items (no bank account details) + // Step indices: 0 = LegalName, 1 = Address, 2 = PhoneNumber + return [ + { + description: translate('personalInfoStep.legalName'), + title: `${personalDetails[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${personalDetails[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`, + shouldShowRightIcon: true, + onPress: () => { + moveToEditStep(0); + }, + }, + { + description: translate('personalInfoStep.address'), + title: `${personalDetails.addressStreet}, ${personalDetails.addressCity}, ${personalDetails.addressState} ${personalDetails.addressZip}`, + shouldShowRightIcon: true, + onPress: () => { + moveToEditStep(1); + }, + }, + { + description: translate('common.phoneNumber'), + title: personalDetails[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER], + shouldShowRightIcon: true, + onPress: () => { + moveToEditStep(2); + }, + }, + ]; + }; + + const summaryItems = getSummaryItems(); + + return ( + + ); +} + +UpdatePersonalInfoConfirmation.displayName = 'UpdatePersonalInfoConfirmation'; + +export default UpdatePersonalInfoConfirmation; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index cb87a2b8ee83b..fa1f8edd57924 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -30,6 +30,7 @@ import type {FormattedSelectedPaymentMethod} from '@hooks/usePaymentMethodState/ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import {filterPersonalCards, maskCardNumber} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -63,6 +64,7 @@ function WalletPage() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [isLoadingPaymentMethods = true] = useOnyx(ONYXKEYS.IS_LOADING_PAYMENT_METHODS, {canBeMissing: true}); const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true}); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); const [walletTerms = getEmptyObject()] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true}); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: false}); const [userAccount] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); @@ -151,6 +153,12 @@ function WalletPage() { }; const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { + // Check if this is a personal bank account missing required personal info + if (isPersonalBankAccountMissingInfo(accountData, privatePersonalDetails)) { + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT); + return; + } + const accountPolicyID = accountData?.additionalData?.policyID; if (accountPolicyID) { From c3a4b82cae0ca08b2ad702704db389b151f0541a Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 21:57:17 +0300 Subject: [PATCH 02/63] chore: hoist function calls and remove unnecessary comments --- src/libs/actions/BankAccounts.ts | 10 ++++------ src/pages/settings/Wallet/PaymentMethodList.tsx | 2 -- src/pages/settings/Wallet/PaymentMethodListItem.tsx | 1 - .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 3 --- .../Wallet/UpdatePersonalInfoConfirmation.tsx | 11 ++++++----- src/pages/settings/Wallet/WalletPage/index.tsx | 1 - 6 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 81dbe7fe1e1f4..8420f97d9f38a 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -152,11 +152,13 @@ function clearPersonalBankAccountErrors() { * This is used when a US personal bank account in OPEN state is missing required info. */ function updatePersonalBankAccountInfo(accountData: Partial) { + const formattedStreet = getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2); + const parameters = { phoneNumber: accountData?.phoneNumber, legalFirstName: accountData?.legalFirstName, legalLastName: accountData?.legalLastName, - addressStreet: getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2), + addressStreet: formattedStreet, addressCity: accountData?.addressCity, addressState: accountData?.addressState, addressZip: accountData?.addressZipCode, @@ -194,7 +196,7 @@ function updatePersonalBankAccountInfo(accountData: Partial void; isPopupMenuVisible: boolean; onThreeDotsPress: () => void}>(null); const handleRowPress = (e: GestureResponderEvent | KeyboardEvent | undefined) => { - // If account needs action (setup or missing info), navigate directly instead of showing menu if (isAccountNeedingAction(item) || !threeDotsMenuItems || (item.cardID && item.onThreeDotsMenuPress)) { item.onPress?.(e); } else if (threeDotsMenuRef.current) { diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 1067dae090226..fbbdc031d0c02 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -34,9 +34,6 @@ function UpdatePersonalBankAccountPage() { updatePersonalBankAccountInfo(accountData); }; - // Skip steps where personal info already exists - // Step indices: 0 = LegalName, 1 = Address, 2 = PhoneNumber, 3 = Confirmation - // getSkippedStepsPersonalInfo returns indices 1, 2, 3 for original flow, but we have 0-indexed steps const skipSteps = getSkippedStepsPersonalInfo(privatePersonalDetails); const { diff --git a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx index 8730572f7187b..407304ca267e3 100644 --- a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx @@ -49,12 +49,13 @@ function UpdatePersonalInfoConfirmation({onNext, onMove, isEditing}: SubStepProp const getSummaryItems = () => { const personalDetails = getPersonalDetails(); + const legalNameLabel = translate('personalInfoStep.legalName'); + const addressLabel = translate('personalInfoStep.address'); + const phoneNumberLabel = translate('common.phoneNumber'); - // Only show personal info items (no bank account details) - // Step indices: 0 = LegalName, 1 = Address, 2 = PhoneNumber return [ { - description: translate('personalInfoStep.legalName'), + description: legalNameLabel, title: `${personalDetails[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${personalDetails[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`, shouldShowRightIcon: true, onPress: () => { @@ -62,7 +63,7 @@ function UpdatePersonalInfoConfirmation({onNext, onMove, isEditing}: SubStepProp }, }, { - description: translate('personalInfoStep.address'), + description: addressLabel, title: `${personalDetails.addressStreet}, ${personalDetails.addressCity}, ${personalDetails.addressState} ${personalDetails.addressZip}`, shouldShowRightIcon: true, onPress: () => { @@ -70,7 +71,7 @@ function UpdatePersonalInfoConfirmation({onNext, onMove, isEditing}: SubStepProp }, }, { - description: translate('common.phoneNumber'), + description: phoneNumberLabel, title: personalDetails[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER], shouldShowRightIcon: true, onPress: () => { diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index fa1f8edd57924..19f5512129fe8 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -153,7 +153,6 @@ function WalletPage() { }; const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { - // Check if this is a personal bank account missing required personal info if (isPersonalBankAccountMissingInfo(accountData, privatePersonalDetails)) { Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT); return; From aa187c7ac80bd5120d5f285794d25b1444a987bb Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 22:01:03 +0300 Subject: [PATCH 03/63] fix: skipped steps indices for update flow (adjust from 1-indexed to 0-indexed) --- src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index fbbdc031d0c02..eec6542190294 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -34,7 +34,10 @@ function UpdatePersonalBankAccountPage() { updatePersonalBankAccountInfo(accountData); }; - const skipSteps = getSkippedStepsPersonalInfo(privatePersonalDetails); + // getSkippedStepsPersonalInfo returns indices 1, 2, 3 (for a flow with an extra leading step) + // Our flow is 0-indexed: 0=LegalName, 1=Address, 2=PhoneNumber, 3=Confirmation + // Adjust by subtracting 1 from each returned index + const skipSteps = getSkippedStepsPersonalInfo(privatePersonalDetails).map((step) => step - 1); const { componentToRender: SubStep, From a2e71f61f90629f06a745b6ca39178f76187d91f Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 22:59:59 +0300 Subject: [PATCH 04/63] fix: update success screen header and description to match design mockups --- src/languages/de.ts | 3 + src/languages/en.ts | 3 + src/languages/es.ts | 3 + src/languages/fr.ts | 3 + src/languages/it.ts | 3 + src/languages/ja.ts | 3 + src/languages/nl.ts | 3 + src/languages/pl.ts | 3 + src/languages/pt-BR.ts | 3 + src/languages/zh-hans.ts | 3 + src/libs/actions/BankAccounts.ts | 1 - .../Wallet/UpdatePersonalBankAccountPage.tsx | 43 ++++++- tests/unit/BankAccountUtilsTest.ts | 119 ++++++++++++++++++ 13 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 tests/unit/BankAccountUtilsTest.ts diff --git a/src/languages/de.ts b/src/languages/de.ts index 0a9aaf12e4e58..78e124bb2d9cb 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3110,6 +3110,9 @@ ${ toGetStarted: 'Fügen Sie ein persönliches Bankkonto hinzu, um Erstattungen zu erhalten, Rechnungen zu bezahlen oder die Expensify Wallet zu aktivieren.', updatePersonalInfo: 'Bankkonto aktualisieren', updatePersonalInfoFailure: 'Die Bankkontoinformationen konnten nicht aktualisiert werden. Bitte versuchen Sie es später erneut.', + updateSuccessTitle: 'Bankkonto aktualisiert!', + updateSuccessHeader: 'Bankkonto aktualisiert', + updateSuccessMessage: 'Glückwunsch, dein Bankkonto ist eingerichtet und bereit, Rückerstattungen zu empfangen.', }, addPersonalBankAccountPage: { enterPassword: 'Expensify-Passwort eingeben', diff --git a/src/languages/en.ts b/src/languages/en.ts index bf5927b13857f..5caf3b661a18b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3094,6 +3094,9 @@ const translations = { toGetStarted: 'Add a personal bank account to receive reimbursements, pay invoices, or enable the Expensify Wallet.', updatePersonalInfo: 'Update bank account', updatePersonalInfoFailure: 'Unable to update bank account information. Please try again later.', + updateSuccessTitle: 'Bank account updated!', + updateSuccessHeader: 'Bank account updated', + updateSuccessMessage: 'Congrats, your bank account is set up and ready to receive reimbursements.', }, addPersonalBankAccountPage: { enterPassword: 'Enter Expensify password', diff --git a/src/languages/es.ts b/src/languages/es.ts index e605fc5fad7c1..99e142925d546 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2828,6 +2828,9 @@ ${amount} para ${merchant} - ${date}`, toGetStarted: 'Agrega una cuenta bancaria personal para recibir reembolsos, pagar facturas o habilitar la Cartera de Expensify.', updatePersonalInfo: 'Actualizar cuenta bancaria', updatePersonalInfoFailure: 'No se pudo actualizar la información de la cuenta bancaria. Por favor, inténtalo de nuevo más tarde.', + updateSuccessTitle: '¡Cuenta bancaria actualizada!', + updateSuccessHeader: 'Cuenta bancaria actualizada', + updateSuccessMessage: 'Enhorabuena, tu cuenta bancaria está configurada y lista para recibir reembolsos.', }, addPersonalBankAccountPage: { enterPassword: 'Escribe tu contraseña de Expensify', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 21acf116819fc..1bcf2e22d6066 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3117,6 +3117,9 @@ ${ toGetStarted: 'Ajoutez un compte bancaire personnel pour recevoir des remboursements, payer des factures ou activer le portefeuille Expensify.', updatePersonalInfo: 'Mettre à jour le compte bancaire', updatePersonalInfoFailure: 'Impossible de mettre à jour les informations du compte bancaire. Veuillez réessayer plus tard.', + updateSuccessTitle: 'Compte bancaire mis à jour !', + updateSuccessHeader: 'Compte bancaire mis à jour', + updateSuccessMessage: 'Félicitations, votre compte bancaire est configuré et prêt à recevoir des remboursements.', }, addPersonalBankAccountPage: { enterPassword: 'Saisissez le mot de passe Expensify', diff --git a/src/languages/it.ts b/src/languages/it.ts index 94166138c631c..30f23fb4f104a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3101,6 +3101,9 @@ ${ toGetStarted: 'Aggiungi un conto bancario personale per ricevere rimborsi, pagare fatture o abilitare il portafoglio Expensify.', updatePersonalInfo: 'Aggiorna conto bancario', updatePersonalInfoFailure: 'Impossibile aggiornare le informazioni del conto bancario. Riprova più tardi.', + updateSuccessTitle: 'Conto bancario aggiornato!', + updateSuccessHeader: 'Conto bancario aggiornato', + updateSuccessMessage: 'Complimenti, il tuo conto bancario è configurato e pronto a ricevere rimborsi.', }, addPersonalBankAccountPage: { enterPassword: 'Inserisci la password di Expensify', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 84dbe276551be..f8866219653d2 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3091,6 +3091,9 @@ ${ toGetStarted: '払い戻しを受け取ったり、請求書を支払ったり、Expensify Wallet を有効にしたりするには、個人の銀行口座を追加します。', updatePersonalInfo: '銀行口座を更新', updatePersonalInfoFailure: '銀行口座情報を更新できませんでした。後でもう一度お試しください。', + updateSuccessTitle: '銀行口座が更新されました!', + updateSuccessHeader: '銀行口座が更新されました', + updateSuccessMessage: 'おめでとうございます。銀行口座の設定が完了し、精算の受け取りができるようになりました。', }, addPersonalBankAccountPage: { enterPassword: 'Expensify のパスワードを入力', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index cc6af81592e41..34e75b28e8370 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3099,6 +3099,9 @@ ${ toGetStarted: 'Voeg een persoonlijke bankrekening toe om vergoedingen te ontvangen, facturen te betalen of de Expensify Wallet in te schakelen.', updatePersonalInfo: 'Bankrekening bijwerken', updatePersonalInfoFailure: 'Kan de bankrekeninggegevens niet bijwerken. Probeer het later opnieuw.', + updateSuccessTitle: 'Bankrekening bijgewerkt!', + updateSuccessHeader: 'Bankrekening bijgewerkt', + updateSuccessMessage: 'Gefeliciteerd, je bankrekening is ingesteld en klaar om terugbetalingen te ontvangen.', }, addPersonalBankAccountPage: { enterPassword: 'Expensify-wachtwoord invoeren', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 0421f6d2c1db1..6b9efe1a5f393 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3093,6 +3093,9 @@ ${ toGetStarted: 'Dodaj osobiste konto bankowe, aby otrzymywać zwroty kosztów, opłacać faktury lub włączyć portfel Expensify.', updatePersonalInfo: 'Aktualizuj konto bankowe', updatePersonalInfoFailure: 'Nie można zaktualizować informacji o koncie bankowym. Spróbuj ponownie później.', + updateSuccessTitle: 'Konto bankowe zaktualizowane!', + updateSuccessHeader: 'Konto bankowe zaktualizowane', + updateSuccessMessage: 'Gratulacje, Twoje konto bankowe jest skonfigurowane i gotowe do przyjmowania zwrotów.', }, addPersonalBankAccountPage: { enterPassword: 'Wprowadź hasło do Expensify', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index bad0f12662c2d..3c3b8daa1b116 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3093,6 +3093,9 @@ ${ toGetStarted: 'Adicione uma conta bancária pessoal para receber reembolsos, pagar faturas ou ativar a Carteira Expensify.', updatePersonalInfo: 'Atualizar conta bancária', updatePersonalInfoFailure: 'Não foi possível atualizar as informações da conta bancária. Por favor, tente novamente mais tarde.', + updateSuccessTitle: 'Conta bancária atualizada!', + updateSuccessHeader: 'Conta bancária atualizada', + updateSuccessMessage: 'Parabéns, sua conta bancária está configurada e pronta para receber reembolsos.', }, addPersonalBankAccountPage: { enterPassword: 'Digite a senha do Expensify', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 4b8d69bde8015..e2821235cafc1 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3050,6 +3050,9 @@ ${ toGetStarted: '添加个人银行账户以接收报销、支付发票或启用 Expensify 钱包。', updatePersonalInfo: '更新银行账户', updatePersonalInfoFailure: '无法更新银行账户信息。请稍后重试。', + updateSuccessTitle: '银行账户已更新!', + updateSuccessHeader: '银行账户已更新', + updateSuccessMessage: '恭喜,您的银行账户已设置完成,可以开始接收报销款了。', }, addPersonalBankAccountPage: { enterPassword: '输入 Expensify 密码', diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 8420f97d9f38a..b48faecdb6c25 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -222,7 +222,6 @@ function updatePersonalBankAccountInfo(accountData: Partial> = [LegalName, Addres function UpdatePersonalBankAccountPage() { const {translate} = useLocalize(); + const styles = useThemeStyles(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); const [personalBankAccountDraft] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); + const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {canBeMissing: true}); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; + + const exitFlow = () => { + Navigation.goBack(ROUTES.SETTINGS_WALLET); + clearPersonalBankAccount(); + }; + const submitPersonalInfo = () => { const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; const accountData = { @@ -65,6 +80,32 @@ function UpdatePersonalBankAccountPage() { prevScreen(); }; + if (shouldShowSuccess) { + return ( + + + + + + + ); + } + return ( { + const completePersonalDetails: PrivatePersonalDetails = { + legalFirstName: 'John', + legalLastName: 'Doe', + phoneNumber: '+15551234567', + addresses: [{street: '123 Main St', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}], + }; + + const usPersonalBankAccount: AccountData = { + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + additionalData: {country: CONST.COUNTRY.US}, + } as AccountData; + + it('returns false for non-personal bank accounts', () => { + const accountData = { + ...usPersonalBankAccount, + type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + }); + + it('returns false for accounts not in OPEN state', () => { + const accountData = { + ...usPersonalBankAccount, + state: CONST.BANK_ACCOUNT.STATE.SETUP, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + }); + + it('returns false for non-US accounts', () => { + const accountData = { + ...usPersonalBankAccount, + additionalData: {country: 'CA'}, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + }); + + it('returns true when legal first name is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + legalFirstName: '', + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when legal last name is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + legalLastName: '', + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when address street is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [{street: '', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when address city is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [{street: '123 Main St', city: '', state: 'NY', zip: '10001', country: 'US', current: true}], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when address state is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [{street: '123 Main St', city: 'New York', state: '', zip: '10001', country: 'US', current: true}], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when address zip is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [{street: '123 Main St', city: 'New York', state: 'NY', zip: '', country: 'US', current: true}], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when addresses array is empty', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when phone number is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + phoneNumber: '', + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns false when all personal info is present', () => { + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, completePersonalDetails)).toBe(false); + }); + + it('returns false when accountData is undefined', () => { + expect(isPersonalBankAccountMissingInfo(undefined, completePersonalDetails)).toBe(false); + }); + + it('returns false when privatePersonalDetails is undefined', () => { + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, undefined)).toBe(true); + }); +}); From 6e9e3e6a7ecda6bd38491410d5c3c175956272ce Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 23:03:35 +0300 Subject: [PATCH 05/63] chore: run prettier --- tests/unit/BankAccountUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 3f5271c7614bf..9cef4eaf8b55c 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -1,7 +1,7 @@ import {isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import CONST from '@src/CONST'; -import type AccountData from '@src/types/onyx/AccountData'; import type {PrivatePersonalDetails} from '@src/types/onyx'; +import type AccountData from '@src/types/onyx/AccountData'; describe('isPersonalBankAccountMissingInfo', () => { const completePersonalDetails: PrivatePersonalDetails = { From 2a7d4a473dd3a53b7ad51a2e979163b609a68be1 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 23:31:25 +0300 Subject: [PATCH 06/63] test: add more comprehensive tests for BankAccountUtils --- tests/unit/BankAccountUtilsTest.ts | 302 ++++++++++++++++++++--------- 1 file changed, 206 insertions(+), 96 deletions(-) diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 9cef4eaf8b55c..b960dce22664e 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -1,119 +1,229 @@ -import {isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; +import {getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, isBankAccountPartiallySetup, isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import CONST from '@src/CONST'; -import type {PrivatePersonalDetails} from '@src/types/onyx'; +import type {Account, BankAccountList, PrivatePersonalDetails, Session} from '@src/types/onyx'; import type AccountData from '@src/types/onyx/AccountData'; -describe('isPersonalBankAccountMissingInfo', () => { - const completePersonalDetails: PrivatePersonalDetails = { - legalFirstName: 'John', - legalLastName: 'Doe', - phoneNumber: '+15551234567', - addresses: [{street: '123 Main St', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}], - }; - - const usPersonalBankAccount: AccountData = { - type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, - state: CONST.BANK_ACCOUNT.STATE.OPEN, - additionalData: {country: CONST.COUNTRY.US}, - } as AccountData; - - it('returns false for non-personal bank accounts', () => { - const accountData = { - ...usPersonalBankAccount, - type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, - } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); - }); +describe('BankAccountUtils', () => { + describe('isPersonalBankAccountMissingInfo', () => { + const completePersonalDetails: PrivatePersonalDetails = { + legalFirstName: 'John', + legalLastName: 'Doe', + phoneNumber: '+15551234567', + addresses: [{street: '123 Main St', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}], + }; - it('returns false for accounts not in OPEN state', () => { - const accountData = { - ...usPersonalBankAccount, - state: CONST.BANK_ACCOUNT.STATE.SETUP, + const usPersonalBankAccount: AccountData = { + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + additionalData: {country: CONST.COUNTRY.US}, } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); - }); - it('returns false for non-US accounts', () => { - const accountData = { - ...usPersonalBankAccount, - additionalData: {country: 'CA'}, - } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + it('returns false for non-personal bank accounts', () => { + const accountData = { + ...usPersonalBankAccount, + type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + }); + + it('returns false for accounts not in OPEN state', () => { + const accountData = { + ...usPersonalBankAccount, + state: CONST.BANK_ACCOUNT.STATE.SETUP, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + }); + + it('returns false for non-US accounts', () => { + const accountData = { + ...usPersonalBankAccount, + additionalData: {country: 'CA'}, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + }); + + it('returns true when legal first name is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + legalFirstName: '', + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when legal last name is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + legalLastName: '', + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when address street is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [{street: '', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when address city is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [{street: '123 Main St', city: '', state: 'NY', zip: '10001', country: 'US', current: true}], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when address state is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [{street: '123 Main St', city: 'New York', state: '', zip: '10001', country: 'US', current: true}], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when address zip is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [{street: '123 Main St', city: 'New York', state: 'NY', zip: '', country: 'US', current: true}], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when addresses array is empty', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + addresses: [], + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns true when phone number is missing', () => { + const details: PrivatePersonalDetails = { + ...completePersonalDetails, + phoneNumber: '', + }; + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + }); + + it('returns false when all personal info is present', () => { + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, completePersonalDetails)).toBe(false); + }); + + it('returns false when accountData is undefined', () => { + expect(isPersonalBankAccountMissingInfo(undefined, completePersonalDetails)).toBe(false); + }); + + it('returns false when privatePersonalDetails is undefined', () => { + expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, undefined)).toBe(true); + }); }); - it('returns true when legal first name is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - legalFirstName: '', - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); + describe('getLastFourDigits', () => { + it('returns last 4 digits of bank account number', () => { + expect(getLastFourDigits('123456789012')).toBe('9012'); + }); - it('returns true when legal last name is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - legalLastName: '', - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); + it('returns entire string if less than 4 characters', () => { + expect(getLastFourDigits('123')).toBe('123'); + }); - it('returns true when address street is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [{street: '', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); + it('returns empty string for empty input', () => { + expect(getLastFourDigits('')).toBe(''); + }); - it('returns true when address city is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [{street: '123 Main St', city: '', state: 'NY', zip: '10001', country: 'US', current: true}], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + it('returns exactly 4 characters for 4-digit input', () => { + expect(getLastFourDigits('1234')).toBe('1234'); + }); }); - it('returns true when address state is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [{street: '123 Main St', city: 'New York', state: '', zip: '10001', country: 'US', current: true}], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); + describe('isBankAccountPartiallySetup', () => { + it('returns true for SETUP state', () => { + expect(isBankAccountPartiallySetup(CONST.BANK_ACCOUNT.STATE.SETUP)).toBe(true); + }); - it('returns true when address zip is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [{street: '123 Main St', city: 'New York', state: 'NY', zip: '', country: 'US', current: true}], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); + it('returns true for VERIFYING state', () => { + expect(isBankAccountPartiallySetup(CONST.BANK_ACCOUNT.STATE.VERIFYING)).toBe(true); + }); - it('returns true when addresses array is empty', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); + it('returns true for PENDING state', () => { + expect(isBankAccountPartiallySetup(CONST.BANK_ACCOUNT.STATE.PENDING)).toBe(true); + }); - it('returns true when phone number is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - phoneNumber: '', - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); + it('returns false for OPEN state', () => { + expect(isBankAccountPartiallySetup(CONST.BANK_ACCOUNT.STATE.OPEN)).toBe(false); + }); + + it('returns false for undefined state', () => { + expect(isBankAccountPartiallySetup(undefined)).toBe(false); + }); - it('returns false when all personal info is present', () => { - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, completePersonalDetails)).toBe(false); + it('returns false for empty string state', () => { + expect(isBankAccountPartiallySetup('')).toBe(false); + }); }); - it('returns false when accountData is undefined', () => { - expect(isPersonalBankAccountMissingInfo(undefined, completePersonalDetails)).toBe(false); + describe('hasPartiallySetupBankAccount', () => { + it('returns true when at least one account is in SETUP state', () => { + const bankAccountList = { + '1': {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, + '2': {accountData: {state: CONST.BANK_ACCOUNT.STATE.SETUP}, bankCurrency: 'USD', bankCountry: 'US'}, + } as unknown as BankAccountList; + expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(true); + }); + + it('returns true when at least one account is in VERIFYING state', () => { + const bankAccountList = { + '1': {accountData: {state: CONST.BANK_ACCOUNT.STATE.VERIFYING}, bankCurrency: 'USD', bankCountry: 'US'}, + } as unknown as BankAccountList; + expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(true); + }); + + it('returns false when all accounts are in OPEN state', () => { + const bankAccountList = { + '1': {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, + '2': {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, + } as unknown as BankAccountList; + expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(false); + }); + + it('returns false for empty bank account list', () => { + expect(hasPartiallySetupBankAccount({})).toBe(false); + }); + + it('returns false for null/undefined bank account list', () => { + expect(hasPartiallySetupBankAccount(undefined)).toBe(false); + }); }); - it('returns false when privatePersonalDetails is undefined', () => { - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, undefined)).toBe(true); + describe('getDefaultCompanyWebsite', () => { + it('returns website URL from email domain when not public domain', () => { + const session: Session = {email: 'user@company.com'} as Session; + const account: Account = {isFromPublicDomain: false} as Account; + expect(getDefaultCompanyWebsite(session, account)).toBe('https://www.company.com'); + }); + + it('returns empty string for public domain when shouldShowPublicDomain is false', () => { + const session: Session = {email: 'user@gmail.com'} as Session; + const account: Account = {isFromPublicDomain: true} as Account; + expect(getDefaultCompanyWebsite(session, account, false)).toBe(''); + }); + + it('returns website URL for public domain when shouldShowPublicDomain is true', () => { + const session: Session = {email: 'user@gmail.com'} as Session; + const account: Account = {isFromPublicDomain: true} as Account; + expect(getDefaultCompanyWebsite(session, account, true)).toBe('https://www.gmail.com'); + }); + + it('handles undefined session email', () => { + const session: Session = {} as Session; + const account: Account = {isFromPublicDomain: false} as Account; + expect(getDefaultCompanyWebsite(session, account)).toBe('https://www.'); + }); + + it('handles undefined session', () => { + const account: Account = {isFromPublicDomain: false} as Account; + expect(getDefaultCompanyWebsite(undefined, account)).toBe('https://www.'); + }); }); }); From cb9dea98e5c4c96e93e66b57674f9b5416f7643f Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 23:40:51 +0300 Subject: [PATCH 07/63] fix: use camelCase property names in BankAccountUtils tests --- tests/unit/BankAccountUtilsTest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index b960dce22664e..2b02184e6163f 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -166,23 +166,23 @@ describe('BankAccountUtils', () => { describe('hasPartiallySetupBankAccount', () => { it('returns true when at least one account is in SETUP state', () => { const bankAccountList = { - '1': {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, - '2': {accountData: {state: CONST.BANK_ACCOUNT.STATE.SETUP}, bankCurrency: 'USD', bankCountry: 'US'}, + accountOne: {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, + accountTwo: {accountData: {state: CONST.BANK_ACCOUNT.STATE.SETUP}, bankCurrency: 'USD', bankCountry: 'US'}, } as unknown as BankAccountList; expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(true); }); it('returns true when at least one account is in VERIFYING state', () => { const bankAccountList = { - '1': {accountData: {state: CONST.BANK_ACCOUNT.STATE.VERIFYING}, bankCurrency: 'USD', bankCountry: 'US'}, + accountOne: {accountData: {state: CONST.BANK_ACCOUNT.STATE.VERIFYING}, bankCurrency: 'USD', bankCountry: 'US'}, } as unknown as BankAccountList; expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(true); }); it('returns false when all accounts are in OPEN state', () => { const bankAccountList = { - '1': {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, - '2': {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, + accountOne: {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, + accountTwo: {accountData: {state: CONST.BANK_ACCOUNT.STATE.OPEN}, bankCurrency: 'USD', bankCountry: 'US'}, } as unknown as BankAccountList; expect(hasPartiallySetupBankAccount(bankAccountList)).toBe(false); }); From 6af069fd1c34164ef1e4944e3a58d23e8f0323a9 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 23:52:19 +0300 Subject: [PATCH 08/63] refactor: use it.each for parameterized tests in BankAccountUtilsTest --- tests/unit/BankAccountUtilsTest.ts | 90 ++++++++++-------------------- 1 file changed, 28 insertions(+), 62 deletions(-) diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 2b02184e6163f..b953af2df4002 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -58,34 +58,15 @@ describe('BankAccountUtils', () => { expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); }); - it('returns true when address street is missing', () => { + it.each([ + {field: 'street', address: {street: '', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}}, + {field: 'city', address: {street: '123 Main St', city: '', state: 'NY', zip: '10001', country: 'US', current: true}}, + {field: 'state', address: {street: '123 Main St', city: 'New York', state: '', zip: '10001', country: 'US', current: true}}, + {field: 'zip', address: {street: '123 Main St', city: 'New York', state: 'NY', zip: '', country: 'US', current: true}}, + ])('returns true when address $field is missing', ({address}) => { const details: PrivatePersonalDetails = { ...completePersonalDetails, - addresses: [{street: '', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); - - it('returns true when address city is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [{street: '123 Main St', city: '', state: 'NY', zip: '10001', country: 'US', current: true}], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); - - it('returns true when address state is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [{street: '123 Main St', city: 'New York', state: '', zip: '10001', country: 'US', current: true}], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); - - it('returns true when address zip is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [{street: '123 Main St', city: 'New York', state: 'NY', zip: '', country: 'US', current: true}], + addresses: [address], }; expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); }); @@ -120,46 +101,31 @@ describe('BankAccountUtils', () => { }); describe('getLastFourDigits', () => { - it('returns last 4 digits of bank account number', () => { - expect(getLastFourDigits('123456789012')).toBe('9012'); - }); - - it('returns entire string if less than 4 characters', () => { - expect(getLastFourDigits('123')).toBe('123'); - }); - - it('returns empty string for empty input', () => { - expect(getLastFourDigits('')).toBe(''); - }); - - it('returns exactly 4 characters for 4-digit input', () => { - expect(getLastFourDigits('1234')).toBe('1234'); + it.each([ + {input: '123456789012', expected: '9012', description: 'long account number'}, + {input: '123', expected: '123', description: 'less than 4 characters'}, + {input: '', expected: '', description: 'empty string'}, + {input: '1234', expected: '1234', description: 'exactly 4 characters'}, + ])('returns $expected for $description', ({input, expected}) => { + expect(getLastFourDigits(input)).toBe(expected); }); }); describe('isBankAccountPartiallySetup', () => { - it('returns true for SETUP state', () => { - expect(isBankAccountPartiallySetup(CONST.BANK_ACCOUNT.STATE.SETUP)).toBe(true); - }); - - it('returns true for VERIFYING state', () => { - expect(isBankAccountPartiallySetup(CONST.BANK_ACCOUNT.STATE.VERIFYING)).toBe(true); - }); - - it('returns true for PENDING state', () => { - expect(isBankAccountPartiallySetup(CONST.BANK_ACCOUNT.STATE.PENDING)).toBe(true); - }); - - it('returns false for OPEN state', () => { - expect(isBankAccountPartiallySetup(CONST.BANK_ACCOUNT.STATE.OPEN)).toBe(false); - }); - - it('returns false for undefined state', () => { - expect(isBankAccountPartiallySetup(undefined)).toBe(false); - }); - - it('returns false for empty string state', () => { - expect(isBankAccountPartiallySetup('')).toBe(false); + it.each([ + {state: CONST.BANK_ACCOUNT.STATE.SETUP, name: 'SETUP'}, + {state: CONST.BANK_ACCOUNT.STATE.VERIFYING, name: 'VERIFYING'}, + {state: CONST.BANK_ACCOUNT.STATE.PENDING, name: 'PENDING'}, + ])('returns true for $name state', ({state}) => { + expect(isBankAccountPartiallySetup(state)).toBe(true); + }); + + it.each([ + {state: CONST.BANK_ACCOUNT.STATE.OPEN, name: 'OPEN'}, + {state: undefined, name: 'undefined'}, + {state: '', name: 'empty string'}, + ])('returns false for $name state', ({state}) => { + expect(isBankAccountPartiallySetup(state)).toBe(false); }); }); From b978637ec2cdee2fe91cbe59a922a6e75e3bfb3a Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 20 Jan 2026 23:59:55 +0300 Subject: [PATCH 09/63] fix: add 'as const' to address country field in tests --- tests/unit/BankAccountUtilsTest.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index b953af2df4002..a2b563d91b332 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -59,10 +59,10 @@ describe('BankAccountUtils', () => { }); it.each([ - {field: 'street', address: {street: '', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}}, - {field: 'city', address: {street: '123 Main St', city: '', state: 'NY', zip: '10001', country: 'US', current: true}}, - {field: 'state', address: {street: '123 Main St', city: 'New York', state: '', zip: '10001', country: 'US', current: true}}, - {field: 'zip', address: {street: '123 Main St', city: 'New York', state: 'NY', zip: '', country: 'US', current: true}}, + {field: 'street', address: {street: '', city: 'New York', state: 'NY', zip: '10001', country: 'US' as const, current: true}}, + {field: 'city', address: {street: '123 Main St', city: '', state: 'NY', zip: '10001', country: 'US' as const, current: true}}, + {field: 'state', address: {street: '123 Main St', city: 'New York', state: '', zip: '10001', country: 'US' as const, current: true}}, + {field: 'zip', address: {street: '123 Main St', city: 'New York', state: 'NY', zip: '', country: 'US' as const, current: true}}, ])('returns true when address $field is missing', ({address}) => { const details: PrivatePersonalDetails = { ...completePersonalDetails, From e4d44e7636697db711ed8624e3accdab80a06190 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 30 Jan 2026 10:57:25 +0300 Subject: [PATCH 10/63] feat: add address and phone fields to BankAccountAdditionalData Add missing fields to BankAccountAdditionalData type: - addressCity, addressState, addressStreet, addressZipCode - companyPhone --- src/types/onyx/BankAccount.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/types/onyx/BankAccount.ts b/src/types/onyx/BankAccount.ts index 54581f3a07168..bb807e7b66369 100644 --- a/src/types/onyx/BankAccount.ts +++ b/src/types/onyx/BankAccount.ts @@ -38,6 +38,21 @@ type BankAccountAdditionalData = { /** Powerform files */ achAuthorizationForm?: FileObject[]; }; + + /** City of the business's address */ + addressCity?: string; + + /** State of the business's address */ + addressState?: string; + + /** Business's street address */ + addressStreet?: string; + + /** Zip code of the business's address */ + addressZipCode?: string; + + /** Phone number of the company */ + companyPhone?: string; }; /** Model of bank account */ From 3896c94f14b1d707dfd99e327b07b80fc93d2938 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 12 Feb 2026 23:13:19 +0300 Subject: [PATCH 11/63] refactor: check additionalData instead of privatePersonalDetails for missing bank account info Per reviewer feedback, isPersonalBankAccountMissingInfo now checks accountData.additionalData fields (firstName, lastName, address, companyPhone) instead of privatePersonalDetails, matching OldDot behavior. --- src/libs/BankAccountUtils.ts | 16 ++- .../settings/Wallet/PaymentMethodList.tsx | 3 +- .../settings/Wallet/WalletPage/index.tsx | 3 +- src/types/onyx/BankAccount.ts | 16 ++- tests/unit/BankAccountUtilsTest.ts | 115 +++++++++--------- 5 files changed, 77 insertions(+), 76 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 8b46468c8175e..b612373d714d3 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -3,7 +3,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type * as OnyxTypes from '@src/types/onyx'; import type AccountData from '@src/types/onyx/AccountData'; -import {getCurrentAddress} from './PersonalDetailsUtils'; function getDefaultCompanyWebsite(session: OnyxEntry, account: OnyxEntry, shouldShowPublicDomain = false): string { return account?.isFromPublicDomain && !shouldShowPublicDomain ? '' : `https://www.${Str.extractEmailDomain(session?.email ?? '')}`; @@ -23,12 +22,12 @@ function hasPartiallySetupBankAccount(bankAccountList: OnyxEntry): boolean { +function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): boolean { // Only applies to personal bank accounts if (accountData?.type !== CONST.BANK_ACCOUNT.TYPE.PERSONAL) { return false; @@ -45,13 +44,12 @@ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined, return false; } - // Check if personal details are missing - const currentAddress = getCurrentAddress(privatePersonalDetails); - const hasLegalName = !!privatePersonalDetails?.legalFirstName && !!privatePersonalDetails?.legalLastName; - const hasAddress = !!currentAddress?.street && !!currentAddress?.city && !!currentAddress?.state && !!currentAddress?.zip; - const hasPhoneNumber = !!privatePersonalDetails?.phoneNumber; + const {additionalData} = accountData; + const hasName = !!additionalData?.firstName && !!additionalData?.lastName; + const hasAddress = !!additionalData?.addressStreet && !!additionalData?.addressCity && !!additionalData?.addressState && !!additionalData?.addressZipCode; + const hasPhone = !!additionalData?.companyPhone; - return !hasLegalName || !hasAddress || !hasPhoneNumber; + return !hasName || !hasAddress || !hasPhone; } export {getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, isBankAccountPartiallySetup, isPersonalBankAccountMissingInfo}; diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 4b8ab2293ac03..d6059cd6a40e0 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -406,7 +406,7 @@ function PaymentMethodList({ methodID: paymentMethod.methodID, description: paymentMethod.description, }; - const isMissingPersonalInfo = isPersonalBankAccountMissingInfo(paymentMethod.accountData, privatePersonalDetails); + const isMissingPersonalInfo = isPersonalBankAccountMissingInfo(paymentMethod.accountData); return { ...paymentMethod, @@ -455,7 +455,6 @@ function PaymentMethodList({ activePaymentMethodID, actionPaymentMethodType, onThreeDotsMenuPress, - privatePersonalDetails, policiesForAssignedCards, ]); diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 5a05c5b67d624..61672f1ea9a09 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -70,7 +70,6 @@ function WalletPage() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [isLoadingPaymentMethods = true] = useOnyx(ONYXKEYS.IS_LOADING_PAYMENT_METHODS, {canBeMissing: true}); const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true}); - const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); const [walletTerms = getEmptyObject()] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true}); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: false}); const [userAccount] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); @@ -162,7 +161,7 @@ function WalletPage() { }; const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { - if (isPersonalBankAccountMissingInfo(accountData, privatePersonalDetails)) { + if (isPersonalBankAccountMissingInfo(accountData)) { Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT); return; } diff --git a/src/types/onyx/BankAccount.ts b/src/types/onyx/BankAccount.ts index bb807e7b66369..518075a345da9 100644 --- a/src/types/onyx/BankAccount.ts +++ b/src/types/onyx/BankAccount.ts @@ -39,19 +39,25 @@ type BankAccountAdditionalData = { achAuthorizationForm?: FileObject[]; }; - /** City of the business's address */ + /** First name of the bank account owner */ + firstName?: string; + + /** Last name of the bank account owner */ + lastName?: string; + + /** City of the bank account owner's address */ addressCity?: string; - /** State of the business's address */ + /** State of the bank account owner's address */ addressState?: string; - /** Business's street address */ + /** Street address of the bank account owner */ addressStreet?: string; - /** Zip code of the business's address */ + /** Zip code of the bank account owner's address */ addressZipCode?: string; - /** Phone number of the company */ + /** Phone number of the bank account owner */ companyPhone?: string; }; diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index a2b563d91b332..1a50cdbacdd0e 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -1,102 +1,101 @@ import {getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, isBankAccountPartiallySetup, isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import CONST from '@src/CONST'; -import type {Account, BankAccountList, PrivatePersonalDetails, Session} from '@src/types/onyx'; +import type {Account, BankAccountList, Session} from '@src/types/onyx'; import type AccountData from '@src/types/onyx/AccountData'; describe('BankAccountUtils', () => { describe('isPersonalBankAccountMissingInfo', () => { - const completePersonalDetails: PrivatePersonalDetails = { - legalFirstName: 'John', - legalLastName: 'Doe', - phoneNumber: '+15551234567', - addresses: [{street: '123 Main St', city: 'New York', state: 'NY', zip: '10001', country: 'US', current: true}], - }; - - const usPersonalBankAccount: AccountData = { + const completeAccountData: AccountData = { type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, state: CONST.BANK_ACCOUNT.STATE.OPEN, - additionalData: {country: CONST.COUNTRY.US}, + additionalData: { + country: CONST.COUNTRY.US, + firstName: 'John', + lastName: 'Doe', + addressStreet: '123 Main St', + addressCity: 'New York', + addressState: 'NY', + addressZipCode: '10001', + companyPhone: '+15551234567', + }, } as AccountData; it('returns false for non-personal bank accounts', () => { const accountData = { - ...usPersonalBankAccount, + ...completeAccountData, type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); }); it('returns false for accounts not in OPEN state', () => { const accountData = { - ...usPersonalBankAccount, + ...completeAccountData, state: CONST.BANK_ACCOUNT.STATE.SETUP, } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); }); it('returns false for non-US accounts', () => { const accountData = { - ...usPersonalBankAccount, - additionalData: {country: 'CA'}, + ...completeAccountData, + additionalData: {...completeAccountData.additionalData, country: 'CA'}, } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData, completePersonalDetails)).toBe(false); + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); }); - it('returns true when legal first name is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - legalFirstName: '', - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + it('returns true when firstName is missing', () => { + const accountData = { + ...completeAccountData, + additionalData: {...completeAccountData.additionalData, firstName: ''}, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); - it('returns true when legal last name is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - legalLastName: '', - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + it('returns true when lastName is missing', () => { + const accountData = { + ...completeAccountData, + additionalData: {...completeAccountData.additionalData, lastName: ''}, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); it.each([ - {field: 'street', address: {street: '', city: 'New York', state: 'NY', zip: '10001', country: 'US' as const, current: true}}, - {field: 'city', address: {street: '123 Main St', city: '', state: 'NY', zip: '10001', country: 'US' as const, current: true}}, - {field: 'state', address: {street: '123 Main St', city: 'New York', state: '', zip: '10001', country: 'US' as const, current: true}}, - {field: 'zip', address: {street: '123 Main St', city: 'New York', state: 'NY', zip: '', country: 'US' as const, current: true}}, - ])('returns true when address $field is missing', ({address}) => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [address], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); - }); - - it('returns true when addresses array is empty', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - addresses: [], - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + {field: 'addressStreet', override: {addressStreet: ''}}, + {field: 'addressCity', override: {addressCity: ''}}, + {field: 'addressState', override: {addressState: ''}}, + {field: 'addressZipCode', override: {addressZipCode: ''}}, + ])('returns true when $field is missing', ({override}) => { + const accountData = { + ...completeAccountData, + additionalData: {...completeAccountData.additionalData, ...override}, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); - it('returns true when phone number is missing', () => { - const details: PrivatePersonalDetails = { - ...completePersonalDetails, - phoneNumber: '', - }; - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, details)).toBe(true); + it('returns true when companyPhone is missing', () => { + const accountData = { + ...completeAccountData, + additionalData: {...completeAccountData.additionalData, companyPhone: ''}, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); - it('returns false when all personal info is present', () => { - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, completePersonalDetails)).toBe(false); + it('returns false when all info is present', () => { + expect(isPersonalBankAccountMissingInfo(completeAccountData)).toBe(false); }); it('returns false when accountData is undefined', () => { - expect(isPersonalBankAccountMissingInfo(undefined, completePersonalDetails)).toBe(false); + expect(isPersonalBankAccountMissingInfo(undefined)).toBe(false); }); - it('returns false when privatePersonalDetails is undefined', () => { - expect(isPersonalBankAccountMissingInfo(usPersonalBankAccount, undefined)).toBe(true); + it('returns true when additionalData is missing', () => { + const accountData = { + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + additionalData: {country: CONST.COUNTRY.US}, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); }); From 88e013dfd58491251210dfee823e185afd6941c2 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 12 Feb 2026 23:37:30 +0300 Subject: [PATCH 12/63] chore: add optimistic BANK_ACCOUNT_LIST update, remove useMemo and obvious comments --- src/libs/BankAccountUtils.ts | 10 +---- src/libs/actions/BankAccounts.ts | 39 ++++++++++++++++--- .../settings/Wallet/PaymentMethodListItem.tsx | 18 ++++----- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index b612373d714d3..10fb917cd1c64 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -22,24 +22,18 @@ function hasPartiallySetupBankAccount(bankAccountList: OnyxEntry) { const formattedStreet = getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2); @@ -165,7 +162,34 @@ function updatePersonalBankAccountInfo(accountData: Partial = { + const bankAccountListUpdates: Record< + string, + { + accountData: { + additionalData: {firstName?: string; lastName?: string; addressStreet?: string; addressCity?: string; addressState?: string; addressZipCode?: string; companyPhone?: string}; + }; + } + > = {}; + for (const [key, bankAccount] of Object.entries(bankAccountList ?? {})) { + if (!isPersonalBankAccountMissingInfo(bankAccount?.accountData)) { + continue; + } + bankAccountListUpdates[key] = { + accountData: { + additionalData: { + firstName: parameters.legalFirstName, + lastName: parameters.legalLastName, + addressStreet: parameters.addressStreet, + addressCity: parameters.addressCity, + addressState: parameters.addressState, + addressZipCode: parameters.addressZip, + companyPhone: parameters.phoneNumber, + }, + }, + }; + } + + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -207,6 +231,11 @@ function updatePersonalBankAccountInfo(accountData: Partial { - if (isNeedingAction) { - return translate('common.actionRequired'); - } - return shouldShowDefaultBadge ? translate('paymentMethodList.defaultPaymentMethod') : undefined; - }, [isNeedingAction, shouldShowDefaultBadge, translate]); + + let badgeText; + if (isNeedingAction) { + badgeText = translate('common.actionRequired'); + } else if (shouldShowDefaultBadge) { + badgeText = translate('paymentMethodList.defaultPaymentMethod'); + } return ( Date: Fri, 13 Feb 2026 00:47:50 +0300 Subject: [PATCH 13/63] refactor: migrate to useSubPage --- src/CONST/index.ts | 9 +++ src/ROUTES.ts | 10 ++- src/libs/Navigation/linkingConfig/config.ts | 3 +- src/libs/Navigation/types.ts | 5 +- src/libs/actions/BankAccounts.ts | 10 ++- .../settings/Wallet/PaymentMethodListItem.tsx | 6 +- .../Wallet/UpdatePersonalBankAccountPage.tsx | 69 +++++++++++-------- .../Wallet/UpdatePersonalInfoConfirmation.tsx | 4 +- .../settings/Wallet/WalletPage/index.tsx | 2 +- 9 files changed, 76 insertions(+), 42 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 4eb625affacd1..5844d32fe500e 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -2777,6 +2777,15 @@ const CONST = { VENDOR_BILL: 'VENDOR_BILL', }, + UPDATE_PERSONAL_BANK_ACCOUNT: { + PAGE_NAME: { + LEGAL_NAME: 'legal-name', + ADDRESS: 'address', + PHONE_NUMBER: 'phone-number', + CONFIRM: 'confirm', + }, + }, + MISSING_PERSONAL_DETAILS: { STEP_INDEX_LIST: ['1', '2', '3', '4'], PAGE_NAME: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bb42b87c7c18a..c4a28f4ce0a46 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -416,7 +416,15 @@ const ROUTES = { SETTINGS_ADD_US_BANK_ACCOUNT: 'settings/wallet/add-us-bank-account', SETTINGS_ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'settings/wallet/add-us-bank-account/entry-point', - SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT: 'settings/wallet/update-personal-bank-account', + SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT: { + route: 'settings/wallet/update-personal-bank-account/:subPage?/:action?', + getRoute: (subPage?: string, action?: 'edit') => { + if (!subPage) { + return 'settings/wallet/update-personal-bank-account' as const; + } + return `settings/wallet/update-personal-bank-account/${subPage}${action ? `/${action}` : ''}` as const; + }, + }, SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: `settings/wallet/add-bank-account/select-country/${VERIFY_ACCOUNT}`, SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', SETTINGS_WALLET_UNSHARE_BANK_ACCOUNT: { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index bf323cff27852..7f5d004491da5 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -346,8 +346,7 @@ const config: LinkingOptions['config'] = { exact: true, }, [SCREENS.SETTINGS.UPDATE_PERSONAL_BANK_ACCOUNT]: { - path: ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT, - exact: true, + path: ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.route, }, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: { path: ROUTES.SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 142d68cb71cb5..80242ecd33a79 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -241,7 +241,10 @@ type SettingsNavigatorParamList = { }; [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: undefined; [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT_ENTRY_POINT]: undefined; - [SCREENS.SETTINGS.UPDATE_PERSONAL_BANK_ACCOUNT]: undefined; + [SCREENS.SETTINGS.UPDATE_PERSONAL_BANK_ACCOUNT]: { + subPage?: string; + action?: 'edit'; + }; [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: undefined; [SCREENS.SETTINGS.RULES.ADD]: undefined; [SCREENS.SETTINGS.RULES.ADD_MERCHANT]: undefined; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 39a405453c6b1..d5120792a7159 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -189,7 +189,9 @@ function updatePersonalBankAccountInfo(accountData: Partial = { + const onyxData: OnyxData< + typeof ONYXKEYS.PERSONAL_BANK_ACCOUNT | typeof ONYXKEYS.PRIVATE_PERSONAL_DETAILS | typeof ONYXKEYS.BANK_ACCOUNT_LIST | typeof ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT + > = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -236,6 +238,11 @@ function updatePersonalBankAccountInfo(accountData: Partial { - if (isAccountNeedingAction(item) || !showThreeDotsMenu || (item.cardID && item.onThreeDotsMenuPress)) { + if (isNeedingAction || !showThreeDotsMenu || (item.cardID && item.onThreeDotsMenuPress)) { item.onPress?.(e); } else if (threeDotsMenuRef.current) { threeDotsMenuRef.current.onThreeDotsPress(); } }; - const isNeedingAction = isAccountNeedingAction(item); - let badgeText; if (isNeedingAction) { badgeText = translate('common.actionRequired'); diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 02fec89adea5d..abdc772aa0813 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,13 +1,12 @@ import React from 'react'; import ConfirmationPage from '@components/ConfirmationPage'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import useSubPage from '@hooks/useSubPage'; import useThemeStyles from '@hooks/useThemeStyles'; import {formatE164PhoneNumber} from '@libs/LoginUtils'; import Navigation from '@navigation/Navigation'; @@ -21,7 +20,17 @@ import PhoneNumber from './InternationalDepositAccount/PersonalInfo/substeps/Pho import getSkippedStepsPersonalInfo from './InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo'; import UpdatePersonalInfoConfirmation from './UpdatePersonalInfoConfirmation'; -const bodyContent: Array> = [LegalName, Address, PhoneNumber, UpdatePersonalInfoConfirmation]; +const PAGE_NAME = CONST.UPDATE_PERSONAL_BANK_ACCOUNT.PAGE_NAME; + +// getSkippedStepsPersonalInfo returns 1-based indices for a flow with an extra leading step +const STEP_INDEX_TO_PAGE_NAME: string[] = [PAGE_NAME.LEGAL_NAME, PAGE_NAME.ADDRESS, PAGE_NAME.PHONE_NUMBER]; + +const formPages = [ + {pageName: PAGE_NAME.LEGAL_NAME, component: LegalName}, + {pageName: PAGE_NAME.ADDRESS, component: Address}, + {pageName: PAGE_NAME.PHONE_NUMBER, component: PhoneNumber}, + {pageName: PAGE_NAME.CONFIRM, component: UpdatePersonalInfoConfirmation}, +]; function UpdatePersonalBankAccountPage() { const {translate} = useLocalize(); @@ -49,35 +58,31 @@ function UpdatePersonalBankAccountPage() { updatePersonalBankAccountInfo(accountData); }; - // getSkippedStepsPersonalInfo returns indices 1, 2, 3 (for a flow with an extra leading step) - // Our flow is 0-indexed: 0=LegalName, 1=Address, 2=PhoneNumber, 3=Confirmation - // Adjust by subtracting 1 from each returned index - const skipSteps = getSkippedStepsPersonalInfo(privatePersonalDetails).map((step) => step - 1); - - const { - componentToRender: SubStep, - isEditing, - nextScreen, - prevScreen, - moveTo, - screenIndex, - goToTheLastStep, - } = useSubStep({ - bodyContent, - skipSteps, + const skipPages = getSkippedStepsPersonalInfo(privatePersonalDetails) + .map((step) => STEP_INDEX_TO_PAGE_NAME.at(step - 1)) + .filter((name): name is string => !!name); + + const {CurrentPage, isEditing, currentPageName, prevPage, nextPage, moveTo, isRedirecting} = useSubPage({ + pages: formPages, onFinished: submitPersonalInfo, + skipPages, + buildRoute: (pageName, action) => ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(pageName, action), }); + if (isRedirecting) { + return ; + } + const handleBackButtonPress = () => { if (isEditing) { - goToTheLastStep(); + Navigation.goBack(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(PAGE_NAME.CONFIRM)); return; } - if (screenIndex === 0) { + if (currentPageName === PAGE_NAME.LEGAL_NAME) { Navigation.goBack(); return; } - prevScreen(); + prevPage(); }; if (shouldShowSuccess) { @@ -107,17 +112,21 @@ function UpdatePersonalBankAccountPage() { } return ( - - + - + ); } diff --git a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx index 407304ca267e3..3521d334b39f5 100644 --- a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx @@ -2,7 +2,7 @@ import React from 'react'; import CommonConfirmationStep from '@components/SubStepForms/ConfirmationStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import {formatE164PhoneNumber} from '@libs/LoginUtils'; import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; @@ -15,7 +15,7 @@ const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; const DEFAULT_OBJECT = {}; -function UpdatePersonalInfoConfirmation({onNext, onMove, isEditing}: SubStepProps) { +function UpdatePersonalInfoConfirmation({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 61672f1ea9a09..51a9b83d2738c 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -162,7 +162,7 @@ function WalletPage() { const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { if (isPersonalBankAccountMissingInfo(accountData)) { - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT); + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute()); return; } From 6083f0ed4fce679d238b1422123a02fb00a2236b Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 13 Feb 2026 01:09:19 +0300 Subject: [PATCH 14/63] fix: improve unit test coverage with undefined fields and edge cases --- tests/unit/BankAccountUtilsTest.ts | 63 +++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 1a50cdbacdd0e..1718dd9c9f858 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -44,28 +44,35 @@ describe('BankAccountUtils', () => { expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); }); - it('returns true when firstName is missing', () => { + it('returns false when country is undefined on additionalData', () => { const accountData = { ...completeAccountData, - additionalData: {...completeAccountData.additionalData, firstName: ''}, + additionalData: {...completeAccountData.additionalData, country: undefined}, } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); + }); + + it('returns false when accountData is undefined', () => { + expect(isPersonalBankAccountMissingInfo(undefined)).toBe(false); }); - it('returns true when lastName is missing', () => { + it('returns false when additionalData is undefined', () => { const accountData = { - ...completeAccountData, - additionalData: {...completeAccountData.additionalData, lastName: ''}, + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, + state: CONST.BANK_ACCOUNT.STATE.OPEN, } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); }); it.each([ + {field: 'firstName', override: {firstName: ''}}, + {field: 'lastName', override: {lastName: ''}}, {field: 'addressStreet', override: {addressStreet: ''}}, {field: 'addressCity', override: {addressCity: ''}}, {field: 'addressState', override: {addressState: ''}}, {field: 'addressZipCode', override: {addressZipCode: ''}}, - ])('returns true when $field is missing', ({override}) => { + {field: 'companyPhone', override: {companyPhone: ''}}, + ])('returns true when $field is empty string', ({override}) => { const accountData = { ...completeAccountData, additionalData: {...completeAccountData.additionalData, ...override}, @@ -73,23 +80,23 @@ describe('BankAccountUtils', () => { expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); - it('returns true when companyPhone is missing', () => { + it.each([ + {field: 'firstName', override: {firstName: undefined}}, + {field: 'lastName', override: {lastName: undefined}}, + {field: 'addressStreet', override: {addressStreet: undefined}}, + {field: 'addressCity', override: {addressCity: undefined}}, + {field: 'addressState', override: {addressState: undefined}}, + {field: 'addressZipCode', override: {addressZipCode: undefined}}, + {field: 'companyPhone', override: {companyPhone: undefined}}, + ])('returns true when $field is undefined', ({override}) => { const accountData = { ...completeAccountData, - additionalData: {...completeAccountData.additionalData, companyPhone: ''}, + additionalData: {...completeAccountData.additionalData, ...override}, } as AccountData; expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); - it('returns false when all info is present', () => { - expect(isPersonalBankAccountMissingInfo(completeAccountData)).toBe(false); - }); - - it('returns false when accountData is undefined', () => { - expect(isPersonalBankAccountMissingInfo(undefined)).toBe(false); - }); - - it('returns true when additionalData is missing', () => { + it('returns true when all owner fields are missing (only country present)', () => { const accountData = { type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, state: CONST.BANK_ACCOUNT.STATE.OPEN, @@ -97,6 +104,24 @@ describe('BankAccountUtils', () => { } as AccountData; expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); + + it('returns true when name and phone are missing but address is present', () => { + const accountData = { + ...completeAccountData, + additionalData: { + country: CONST.COUNTRY.US, + addressStreet: '123 Main St', + addressCity: 'New York', + addressState: 'NY', + addressZipCode: '10001', + }, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); + }); + + it('returns false when all info is present', () => { + expect(isPersonalBankAccountMissingInfo(completeAccountData)).toBe(false); + }); }); describe('getLastFourDigits', () => { From d0270ff0f162e0c9fc8db19cfd052797039385b1 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 13 Feb 2026 01:31:29 +0300 Subject: [PATCH 15/63] fix: populate address fields from profile when address step is skipped --- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index abdc772aa0813..a919e608b2330 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -9,6 +9,7 @@ import useOnyx from '@hooks/useOnyx'; import useSubPage from '@hooks/useSubPage'; import useThemeStyles from '@hooks/useThemeStyles'; import {formatE164PhoneNumber} from '@libs/LoginUtils'; +import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; import Navigation from '@navigation/Navigation'; import {clearPersonalBankAccount, updatePersonalBankAccountInfo} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; @@ -49,9 +50,16 @@ function UpdatePersonalBankAccountPage() { }; const submitPersonalInfo = () => { + const currentAddress = getCurrentAddress(privatePersonalDetails); const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; const accountData = { - ...privatePersonalDetails, + legalFirstName: privatePersonalDetails?.legalFirstName, + legalLastName: privatePersonalDetails?.legalLastName, + addressStreet: currentAddress?.street, + addressCity: currentAddress?.city, + addressState: currentAddress?.state, + addressZipCode: currentAddress?.zip, + country: currentAddress?.country, ...personalBankAccountDraft, phoneNumber: formatE164PhoneNumber(finalPhoneNumber, countryCode), }; From 5150e63b9c0130da96f3774e043b4202e6071d59 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Sun, 1 Mar 2026 14:25:20 +0300 Subject: [PATCH 16/63] fix: optimistic bank account update, and back button for skipped steps --- src/libs/BankAccountUtils.ts | 4 +- src/libs/actions/BankAccounts.ts | 40 +++++++++++++------ .../settings/Wallet/PaymentMethodListItem.tsx | 4 +- .../Wallet/UpdatePersonalBankAccountPage.tsx | 12 +++--- .../Wallet/UpdatePersonalInfoConfirmation.tsx | 8 ++-- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 10fb917cd1c64..dc78c99a937cd 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -30,11 +30,11 @@ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): return false; } - if (accountData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN) { + if (accountData.state !== CONST.BANK_ACCOUNT.STATE.OPEN) { return false; } - if (accountData?.additionalData?.country !== CONST.COUNTRY.US) { + if (accountData.additionalData?.country !== CONST.COUNTRY.US) { return false; } diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 92e781027127d..04528ab019c72 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -162,18 +162,14 @@ function updatePersonalBankAccountInfo(accountData: Partial = {}; + type AdditionalDataUpdate = {firstName?: string; lastName?: string; addressStreet?: string; addressCity?: string; addressState?: string; addressZipCode?: string; companyPhone?: string}; + const bankAccountListUpdates: Record = {}; + const bankAccountListRollback: Record = {}; for (const [key, bankAccount] of Object.entries(bankAccountList ?? {})) { if (!isPersonalBankAccountMissingInfo(bankAccount?.accountData)) { continue; } + const prevData = bankAccount?.accountData?.additionalData; bankAccountListUpdates[key] = { accountData: { additionalData: { @@ -187,6 +183,19 @@ function updatePersonalBankAccountInfo(accountData: Partial void; isPopupMenuVisible: boolean; onThreeDotsPress: () => void}>(null); const isInSetupState = isAccountInSetupState(item); - const showThreeDotsMenu = item.shouldShowThreeDotsMenu !== false && !!threeDotsMenuItems && !isInSetupState; + const showThreeDotsMenu = item.shouldShowThreeDotsMenu !== false && !!threeDotsMenuItems && !isInSetupState && !item.isMissingPersonalInfo; const isNeedingAction = isAccountNeedingAction(item); @@ -172,7 +172,7 @@ function PaymentMethodListItem({item, shouldShowDefaultBadge, threeDotsMenuItems badgeSuccess={isNeedingAction ? true : undefined} badgeStyle={item.isCardFrozen ? styles.badgeBordered : undefined} wrapperStyle={[styles.paymentMethod, listItemStyle]} - iconRight={isInSetupState ? undefined : item.iconRight} + iconRight={isNeedingAction ? undefined : item.iconRight} shouldShowRightIcon={!showThreeDotsMenu && item.shouldShowRightIcon} shouldShowRightComponent={showThreeDotsMenu} rightComponent={ diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index a919e608b2330..6c32b6a8ac413 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -37,10 +37,10 @@ function UpdatePersonalBankAccountPage() { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); - const [personalBankAccountDraft] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); - const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {canBeMissing: true}); - const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [personalBankAccountDraft] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; @@ -81,12 +81,14 @@ function UpdatePersonalBankAccountPage() { return ; } + const firstVisiblePage = formPages.find((p) => !skipPages.includes(p.pageName)); + const handleBackButtonPress = () => { if (isEditing) { Navigation.goBack(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(PAGE_NAME.CONFIRM)); return; } - if (currentPageName === PAGE_NAME.LEGAL_NAME) { + if (currentPageName === firstVisiblePage?.pageName) { Navigation.goBack(); return; } diff --git a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx index 3521d334b39f5..b0a918aaba047 100644 --- a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx @@ -18,10 +18,10 @@ const DEFAULT_OBJECT = {}; function UpdatePersonalInfoConfirmation({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); - const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); - const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); - const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {canBeMissing: true}); - const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); const isLoading = personalBankAccount?.isLoading ?? false; const error = getLatestErrorMessage(personalBankAccount ?? DEFAULT_OBJECT); From 13aac84eebd67dee783ba06b8a8ba49b0a9613cd Mon Sep 17 00:00:00 2001 From: TaduJR Date: Sun, 1 Mar 2026 14:37:00 +0300 Subject: [PATCH 17/63] fix: skip to first non-skipped step and remove addresses array replacement --- src/libs/actions/BankAccounts.ts | 12 ------------ .../Wallet/UpdatePersonalBankAccountPage.tsx | 5 ++++- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 04528ab019c72..67cceb1d27816 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -233,18 +233,6 @@ function updatePersonalBankAccountInfo(accountData: Partial STEP_INDEX_TO_PAGE_NAME.at(step - 1)) .filter((name): name is string => !!name); + const firstNonSkippedIndex = formPages.findIndex((p) => !skipPages.includes(p.pageName)); + const {CurrentPage, isEditing, currentPageName, prevPage, nextPage, moveTo, isRedirecting} = useSubPage({ pages: formPages, onFinished: submitPersonalInfo, skipPages, + startFrom: firstNonSkippedIndex >= 0 ? firstNonSkippedIndex : 0, buildRoute: (pageName, action) => ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(pageName, action), }); @@ -81,7 +84,7 @@ function UpdatePersonalBankAccountPage() { return ; } - const firstVisiblePage = formPages.find((p) => !skipPages.includes(p.pageName)); + const firstVisiblePage = formPages.at(firstNonSkippedIndex >= 0 ? firstNonSkippedIndex : 0); const handleBackButtonPress = () => { if (isEditing) { From 58d05d8f60ea359c4de82daa5a7fda4c64a1ff29 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Sun, 1 Mar 2026 15:02:01 +0300 Subject: [PATCH 18/63] fix: default to US when additionalData.country is absent and restore three-dots menu --- src/libs/BankAccountUtils.ts | 9 +++++---- .../settings/Wallet/PaymentMethodListItem.tsx | 2 +- .../Wallet/UpdatePersonalBankAccountPage.tsx | 1 - tests/unit/BankAccountUtilsTest.ts | 15 ++++++++++++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index dc78c99a937cd..07f5741d90fb6 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -21,9 +21,7 @@ function hasPartiallySetupBankAccount(bankAccountList: OnyxEntry void; isPopupMenuVisible: boolean; onThreeDotsPress: () => void}>(null); const isInSetupState = isAccountInSetupState(item); - const showThreeDotsMenu = item.shouldShowThreeDotsMenu !== false && !!threeDotsMenuItems && !isInSetupState && !item.isMissingPersonalInfo; + const showThreeDotsMenu = item.shouldShowThreeDotsMenu !== false && !!threeDotsMenuItems && !isInSetupState; const isNeedingAction = isAccountNeedingAction(item); diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index d2b06034f451f..efc792c536833 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -23,7 +23,6 @@ import UpdatePersonalInfoConfirmation from './UpdatePersonalInfoConfirmation'; const PAGE_NAME = CONST.UPDATE_PERSONAL_BANK_ACCOUNT.PAGE_NAME; -// getSkippedStepsPersonalInfo returns 1-based indices for a flow with an extra leading step const STEP_INDEX_TO_PAGE_NAME: string[] = [PAGE_NAME.LEGAL_NAME, PAGE_NAME.ADDRESS, PAGE_NAME.PHONE_NUMBER]; const formPages = [ diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 1718dd9c9f858..60b99cda4e23b 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -44,7 +44,7 @@ describe('BankAccountUtils', () => { expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); }); - it('returns false when country is undefined on additionalData', () => { + it('defaults to US when country is undefined and returns false when all info is present', () => { const accountData = { ...completeAccountData, additionalData: {...completeAccountData.additionalData, country: undefined}, @@ -52,16 +52,25 @@ describe('BankAccountUtils', () => { expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); }); + it('defaults to US when country is undefined and returns true when info is missing', () => { + const accountData = { + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + additionalData: {country: undefined}, + } as AccountData; + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); + }); + it('returns false when accountData is undefined', () => { expect(isPersonalBankAccountMissingInfo(undefined)).toBe(false); }); - it('returns false when additionalData is undefined', () => { + it('defaults to US when additionalData is undefined and returns true since all fields are missing', () => { const accountData = { type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, state: CONST.BANK_ACCOUNT.STATE.OPEN, } as AccountData; - expect(isPersonalBankAccountMissingInfo(accountData)).toBe(false); + expect(isPersonalBankAccountMissingInfo(accountData)).toBe(true); }); it.each([ From 77de2bc926dfb0956806efc1eaa1fb9c11e0d4f6 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Sun, 1 Mar 2026 15:07:34 +0300 Subject: [PATCH 19/63] fix: clear shared bank account state on unmount to prevent stale data --- src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index efc792c536833..7980e6c87d531 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import ConfirmationPage from '@components/ConfirmationPage'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -41,6 +41,8 @@ function UpdatePersonalBankAccountPage() { const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); + useEffect(() => clearPersonalBankAccount, []); + const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const exitFlow = () => { From 20d965bba0904bf3cf35f5673cd1509eb5968b73 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 2 Mar 2026 11:00:17 +0300 Subject: [PATCH 20/63] fix: clean up shared function side effects and scope shouldDelayAutoFocus to update flow --- src/CONST/index.ts | 1 - src/languages/it.ts | 2 +- .../RELATIONS/SETTINGS_TO_RHP.ts | 1 + src/libs/actions/BankAccounts.ts | 33 +++++- .../PersonalInfo/substeps/AddressStep.tsx | 17 ++- .../PersonalInfo/substeps/PhoneNumberStep.tsx | 8 +- .../settings/Wallet/PaymentMethodList.tsx | 1 - .../Wallet/UpdatePersonalBankAccountPage.tsx | 76 ++++++++----- .../Wallet/UpdatePersonalInfoConfirmation.tsx | 102 ------------------ .../settings/Wallet/WalletPage/index.tsx | 4 +- 10 files changed, 105 insertions(+), 140 deletions(-) delete mode 100644 src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index f238de6fa511b..c8656776b184f 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -2862,7 +2862,6 @@ const CONST = { LEGAL_NAME: 'legal-name', ADDRESS: 'address', PHONE_NUMBER: 'phone-number', - CONFIRM: 'confirm', }, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index fb1a177fdc6ca..cdde3ed2d3c1b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3228,7 +3228,7 @@ ${ currencyHeader: 'Qual è la valuta del tuo conto bancario?', confirmationStepHeader: 'Controlla le tue informazioni.', confirmationStepSubHeader: 'Controlla attentamente i dettagli qui sotto e seleziona la casella delle condizioni per confermare.', - toGetStarted: 'Aggiungi un conto bancario personale per ricevere rimborsi, pagare fatture o attivare l\u2019Expensify Wallet.', + toGetStarted: 'Aggiungi un conto bancario personale per ricevere rimborsi, pagare fatture o attivare l’Expensify Wallet.', updatePersonalInfo: 'Aggiorna conto bancario', updatePersonalInfoFailure: 'Impossibile aggiornare le informazioni del conto bancario. Riprova più tardi.', updateSuccessTitle: 'Conto bancario aggiornato!', diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 4bb401d3b9f9d..0b94a6f9988e5 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -59,6 +59,7 @@ const SETTINGS_TO_RHP: Partial = { optimisticData: [ { @@ -210,6 +215,13 @@ function updatePersonalBankAccountInfo(accountData: Partial { const normalizedAddress = normalizeCountryCode(getCurrentAddress(privatePersonalDetails)) as Address; @@ -43,13 +44,18 @@ function AddressStep({onNext, isEditing}: SubStepProps) { bankAccountPersonalDetails?.country, privatePersonalDetails, ]); + + // homeAddressFormDraft stores draft values saved by shouldSaveDraft on the form inputs. + // These take priority over the address computed above (which comes from PERSONAL_BANK_ACCOUNT_FORM_DRAFT / privatePersonalDetails). + const draftCountry = homeAddressFormDraft?.country; + const draftState = homeAddressFormDraft?.state; const {translate} = useLocalize(); // Check if country is valid const {street} = address ?? {}; const [street1, street2] = street ? street.split('\n') : [undefined, undefined]; - const [currentCountry, setCurrentCountry] = useState(address?.country ?? defaultCountry ?? CONST.COUNTRY.US); - const [state, setState] = useState(address?.state); + const [currentCountry, setCurrentCountry] = useState(draftCountry ?? address?.country ?? defaultCountry ?? CONST.COUNTRY.US); + const [state, setState] = useState(draftState ?? address?.state); const [city, setCity] = useState(address?.city); const [zipcode, setZipcode] = useState(address?.zip); @@ -57,11 +63,11 @@ function AddressStep({onNext, isEditing}: SubStepProps) { if (!address) { return; } - setState(address?.state); - setCurrentCountry(address?.country); + setState(draftState ?? address?.state); + setCurrentCountry(draftCountry ?? address?.country); setCity(address?.city); setZipcode(address?.zip); - }, [address?.state, address?.country, address?.city, address?.zip, address]); + }, [address?.state, address?.country, address?.city, address?.zip, address, draftCountry, draftState]); const handleAddressChange = (value: unknown, key: unknown) => { const addressPart = value as string; @@ -120,6 +126,7 @@ function AddressStep({onNext, isEditing}: SubStepProps) { street1={street1} street2={street2} zip={zipcode} + shouldSaveDraft /> ); diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx index 2b7b00323a993..516a212a5739c 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx @@ -14,7 +14,12 @@ import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.BANK_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.PHONE_NUMBER]; -function PhoneNumberStep({onNext, onMove, isEditing}: SubStepProps) { +type PhoneNumberStepProps = SubStepProps & { + /** Whether to delay auto-focusing the input to avoid conflicts with navigation animations */ + shouldDelayAutoFocus?: boolean; +}; + +function PhoneNumberStep({onNext, onMove, isEditing, shouldDelayAutoFocus}: PhoneNumberStepProps) { const {translate} = useLocalize(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); @@ -58,6 +63,7 @@ function PhoneNumberStep({onNext, onMove, isEditing}: SubStepProps) { inputLabel={translate('common.phoneNumber')} inputMode={CONST.INPUT_MODE.TEL} defaultValue={defaultPhoneNumber} + shouldDelayAutoFocus={shouldDelayAutoFocus} enabledWhenOffline /> ); diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 2fdcf6bcb952f..1090ea9fc47c1 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -439,7 +439,6 @@ function PaymentMethodList({ shouldShowRightIcon, canDismissError: true, isMissingPersonalInfo, - brickRoadIndicator: isMissingPersonalInfo ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }; }); return combinedPaymentMethods; diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 7980e6c87d531..2e1bdac378009 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,37 +1,61 @@ import React, {useEffect} from 'react'; import ConfirmationPage from '@components/ConfirmationPage'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useSubPage from '@hooks/useSubPage'; +import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {formatE164PhoneNumber} from '@libs/LoginUtils'; import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; import Navigation from '@navigation/Navigation'; -import {clearPersonalBankAccount, updatePersonalBankAccountInfo} from '@userActions/BankAccounts'; +import {clearPersonalBankAccount, clearPersonalBankAccountErrors, updatePersonalBankAccountInfo} from '@userActions/BankAccounts'; +import {clearDraftValues, clearErrors} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {PrivatePersonalDetails} from '@src/types/onyx'; import Address from './InternationalDepositAccount/PersonalInfo/substeps/AddressStep'; import LegalName from './InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep'; import PhoneNumber from './InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep'; import getSkippedStepsPersonalInfo from './InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo'; -import UpdatePersonalInfoConfirmation from './UpdatePersonalInfoConfirmation'; const PAGE_NAME = CONST.UPDATE_PERSONAL_BANK_ACCOUNT.PAGE_NAME; -const STEP_INDEX_TO_PAGE_NAME: string[] = [PAGE_NAME.LEGAL_NAME, PAGE_NAME.ADDRESS, PAGE_NAME.PHONE_NUMBER]; +const PAGE_NAMES: string[] = [PAGE_NAME.LEGAL_NAME, PAGE_NAME.ADDRESS, PAGE_NAME.PHONE_NUMBER]; + +/** + * Wrapper that delays auto-focus to avoid validation errors during URL-based navigation transitions. + */ +function DelayedPhoneNumber({isEditing, onNext, onMove}: SubStepProps) { + return ( + + ); +} const formPages = [ {pageName: PAGE_NAME.LEGAL_NAME, component: LegalName}, {pageName: PAGE_NAME.ADDRESS, component: Address}, - {pageName: PAGE_NAME.PHONE_NUMBER, component: PhoneNumber}, - {pageName: PAGE_NAME.CONFIRM, component: UpdatePersonalInfoConfirmation}, + {pageName: PAGE_NAME.PHONE_NUMBER, component: DelayedPhoneNumber}, ]; +/** + * Returns the first non-skipped page name for the update flow. + */ +function getFirstPageName(details?: Partial): string { + const skippedSteps = getSkippedStepsPersonalInfo(details); + const skipPageNames = new Set(skippedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name)); + const firstPage = PAGE_NAMES.find((name) => !skipPageNames.has(name)); + return firstPage ?? PAGE_NAME.LEGAL_NAME; +} + function UpdatePersonalBankAccountPage() { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -41,13 +65,20 @@ function UpdatePersonalBankAccountPage() { const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); - useEffect(() => clearPersonalBankAccount, []); + useEffect(() => { + clearPersonalBankAccountErrors(); + clearErrors(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); + return () => { + clearPersonalBankAccount(); + clearErrors(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); + }; + }, []); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; - const exitFlow = () => { Navigation.goBack(ROUTES.SETTINGS_WALLET); clearPersonalBankAccount(); + clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); }; const submitPersonalInfo = () => { @@ -67,32 +98,22 @@ function UpdatePersonalBankAccountPage() { updatePersonalBankAccountInfo(accountData); }; - const skipPages = getSkippedStepsPersonalInfo(privatePersonalDetails) - .map((step) => STEP_INDEX_TO_PAGE_NAME.at(step - 1)) - .filter((name): name is string => !!name); + const skippedSteps = getSkippedStepsPersonalInfo(privatePersonalDetails); + const skipPages = skippedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); - const firstNonSkippedIndex = formPages.findIndex((p) => !skipPages.includes(p.pageName)); + const firstPageName = getFirstPageName(privatePersonalDetails); + const firstNonSkippedIndex = formPages.findIndex((p) => p.pageName === firstPageName); - const {CurrentPage, isEditing, currentPageName, prevPage, nextPage, moveTo, isRedirecting} = useSubPage({ + const {CurrentPage, currentPageName, prevPage, nextPage} = useSubPage({ pages: formPages, onFinished: submitPersonalInfo, skipPages, startFrom: firstNonSkippedIndex >= 0 ? firstNonSkippedIndex : 0, - buildRoute: (pageName, action) => ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(pageName, action), + buildRoute: (pageName) => ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(pageName), }); - if (isRedirecting) { - return ; - } - - const firstVisiblePage = formPages.at(firstNonSkippedIndex >= 0 ? firstNonSkippedIndex : 0); - const handleBackButtonPress = () => { - if (isEditing) { - Navigation.goBack(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(PAGE_NAME.CONFIRM)); - return; - } - if (currentPageName === firstVisiblePage?.pageName) { + if (currentPageName === firstPageName) { Navigation.goBack(); return; } @@ -136,9 +157,9 @@ function UpdatePersonalBankAccountPage() { onBackButtonPress={handleBackButtonPress} /> {}} /> ); @@ -147,3 +168,4 @@ function UpdatePersonalBankAccountPage() { UpdatePersonalBankAccountPage.displayName = 'UpdatePersonalBankAccountPage'; export default UpdatePersonalBankAccountPage; +export {getFirstPageName}; diff --git a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx b/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx deleted file mode 100644 index b0a918aaba047..0000000000000 --- a/src/pages/settings/Wallet/UpdatePersonalInfoConfirmation.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import CommonConfirmationStep from '@components/SubStepForms/ConfirmationStep'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import type {SubPageProps} from '@hooks/useSubPage/types'; -import {getLatestErrorMessage} from '@libs/ErrorUtils'; -import {formatE164PhoneNumber} from '@libs/LoginUtils'; -import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; -import {clearPersonalBankAccountErrors} from '@userActions/BankAccounts'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; - -const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; - -const DEFAULT_OBJECT = {}; - -function UpdatePersonalInfoConfirmation({onNext, onMove, isEditing}: SubPageProps) { - const {translate} = useLocalize(); - - const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); - const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); - const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); - const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); - - const isLoading = personalBankAccount?.isLoading ?? false; - const error = getLatestErrorMessage(personalBankAccount ?? DEFAULT_OBJECT); - - const getPersonalDetails = () => { - const currentAddress = getCurrentAddress(privatePersonalDetails); - const phone = bankAccountPersonalDetails?.phoneNumber ?? privatePersonalDetails?.phoneNumber; - return { - phoneNumber: (phone && formatE164PhoneNumber(phone, countryCode)) ?? '', - legalFirstName: bankAccountPersonalDetails?.legalFirstName ?? privatePersonalDetails?.legalFirstName ?? '', - legalLastName: bankAccountPersonalDetails?.legalLastName ?? privatePersonalDetails?.legalLastName ?? '', - addressStreet: bankAccountPersonalDetails?.addressStreet ?? currentAddress?.street ?? '', - addressCity: bankAccountPersonalDetails?.addressCity ?? currentAddress?.city ?? '', - addressState: bankAccountPersonalDetails?.addressState ?? currentAddress?.state ?? '', - addressZip: bankAccountPersonalDetails?.addressZipCode ?? currentAddress?.zip ?? '', - }; - }; - - const moveToEditStep = (step: number) => { - if (error) { - clearPersonalBankAccountErrors(); - } - onMove(step); - }; - - const getSummaryItems = () => { - const personalDetails = getPersonalDetails(); - const legalNameLabel = translate('personalInfoStep.legalName'); - const addressLabel = translate('personalInfoStep.address'); - const phoneNumberLabel = translate('common.phoneNumber'); - - return [ - { - description: legalNameLabel, - title: `${personalDetails[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${personalDetails[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`, - shouldShowRightIcon: true, - onPress: () => { - moveToEditStep(0); - }, - }, - { - description: addressLabel, - title: `${personalDetails.addressStreet}, ${personalDetails.addressCity}, ${personalDetails.addressState} ${personalDetails.addressZip}`, - shouldShowRightIcon: true, - onPress: () => { - moveToEditStep(1); - }, - }, - { - description: phoneNumberLabel, - title: personalDetails[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER], - shouldShowRightIcon: true, - onPress: () => { - moveToEditStep(2); - }, - }, - ]; - }; - - const summaryItems = getSummaryItems(); - - return ( - - ); -} - -UpdatePersonalInfoConfirmation.displayName = 'UpdatePersonalInfoConfirmation'; - -export default UpdatePersonalInfoConfirmation; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 4696e09cff5fd..03ef05c8453fb 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -42,6 +42,7 @@ import {getDescriptionForPolicyDomainCard, hasActiveAdminWorkspaces, hasEligible import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import WalletTravelCVVSection from '@pages/settings/Wallet/TravelCVVPage/WalletTravelCVVSection'; +import {getFirstPageName} from '@pages/settings/Wallet/UpdatePersonalBankAccountPage'; import {deletePaymentBankAccount, openPersonalBankAccountSetupView, setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; import {close as closeModal} from '@userActions/Modal'; @@ -75,6 +76,7 @@ function WalletPage() { const [userAccount] = useOnyx(ONYXKEYS.ACCOUNT); const [lastUsedPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); const isUserValidated = userAccount?.validated ?? false; const {isAccountLocked} = useLockedAccountState(); const {showLockedAccountModal} = useLockedAccountActions(); @@ -163,7 +165,7 @@ function WalletPage() { const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { if (isPersonalBankAccountMissingInfo(accountData)) { - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute()); + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(privatePersonalDetails))); return; } From 4b29224b6da07d6929b983d16fd9d84544dbfaec Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 2 Mar 2026 12:04:26 +0300 Subject: [PATCH 21/63] fix: clear stale drafts on mount instead of unmount --- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 2e1bdac378009..16daf700a652f 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -12,7 +12,7 @@ import {formatE164PhoneNumber} from '@libs/LoginUtils'; import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; import Navigation from '@navigation/Navigation'; import {clearPersonalBankAccount, clearPersonalBankAccountErrors, updatePersonalBankAccountInfo} from '@userActions/BankAccounts'; -import {clearDraftValues, clearErrors} from '@userActions/FormActions'; +import {clearDraftValues} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -67,11 +67,8 @@ function UpdatePersonalBankAccountPage() { useEffect(() => { clearPersonalBankAccountErrors(); - clearErrors(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - return () => { - clearPersonalBankAccount(); - clearErrors(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - }; + clearPersonalBankAccount(); + clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); }, []); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; From 2d4e88d81e01c61b78a1fa34bfb7827cc7bcf40c Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 5 Mar 2026 16:04:29 +0300 Subject: [PATCH 22/63] fix: duplicate error display by removing redundant HOME_ADDRESS_FORM failureData and page-level DotIndicatorMessage --- src/libs/BankAccountUtils.ts | 2 ++ src/libs/actions/BankAccounts.ts | 7 ------- .../Wallet/UpdatePersonalBankAccountPage.tsx | 13 ++++++++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 66f8af8d3ef9d..e46fedde6cc6e 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -35,6 +35,8 @@ function hasPartiallySetupBankAccount(bankAccountList: OnyxEntry { - clearPersonalBankAccountErrors(); clearPersonalBankAccount(); clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); }, []); From 071e1bf4305f127e9e25b9eb146bbc873dfe3236 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 5 Mar 2026 16:17:01 +0300 Subject: [PATCH 23/63] fix: duplicate error display and remove stale badgeStyle prop --- src/libs/BankAccountUtils.ts | 2 -- src/pages/settings/Wallet/PaymentMethodListItem.tsx | 1 - .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 10 +--------- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index e46fedde6cc6e..66f8af8d3ef9d 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -35,8 +35,6 @@ function hasPartiallySetupBankAccount(bankAccountList: OnyxEntry Date: Thu, 5 Mar 2026 21:58:15 +0300 Subject: [PATCH 24/63] fix: prevent crash when all update flow pages are skipped --- src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 8d827afeb0d06..833a05865fec5 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -95,7 +95,8 @@ function UpdatePersonalBankAccountPage() { }; const skippedSteps = getSkippedStepsPersonalInfo(privatePersonalDetails); - const skipPages = skippedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); + const skipPageCandidates = skippedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); + const skipPages = skipPageCandidates.length >= formPages.length ? [] : skipPageCandidates; const firstPageName = getFirstPageName(privatePersonalDetails); const firstNonSkippedIndex = formPages.findIndex((p) => p.pageName === firstPageName); From 2a9706b460c92809c8a1ebae356d67f430acc34b Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 5 Mar 2026 22:13:11 +0300 Subject: [PATCH 25/63] fix: remove unused WalletTravelCVVSection import in WalletPage --- src/pages/settings/Wallet/WalletPage/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 4aab0d41592f8..a85b4ab63f90a 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -43,7 +43,6 @@ import {formatPaymentMethods, getPaymentMethodDescription} from '@libs/PaymentUt import {getActiveAdminWorkspaces, getDescriptionForPolicyDomainCard, hasActiveAdminWorkspaces, hasEligibleActiveAdminFromWorkspaces, isPaidGroupPolicy} from '@libs/PolicyUtils'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; -import WalletTravelCVVSection from '@pages/settings/Wallet/TravelCVVPage/WalletTravelCVVSection'; import {getFirstPageName} from '@pages/settings/Wallet/UpdatePersonalBankAccountPage'; import {deletePaymentBankAccount, openPersonalBankAccountSetupView, setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; From 4bad629643b4cc5e7822d42dd8b9f7e592e10b05 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 01:51:31 +0300 Subject: [PATCH 26/63] fix: add bankAccountID param and fix companyPhone in UpdatePersonalBankAccountInfo --- .../UpdatePersonalBankAccountInfoParams.ts | 3 +- src/libs/actions/BankAccounts.ts | 29 +++++++++++-------- .../Wallet/UpdatePersonalBankAccountPage.tsx | 13 ++++----- .../settings/Wallet/WalletPage/index.tsx | 5 +++- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts b/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts index afb3c7c2d3c5a..99a50bd6a6654 100644 --- a/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts +++ b/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts @@ -1,5 +1,6 @@ type UpdatePersonalBankAccountInfoParams = { - phoneNumber?: string; + bankAccountID?: number; + companyPhone?: string; legalFirstName?: string; legalLastName?: string; addressStreet?: string; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 66adda33ade23..530119d62247e 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -152,24 +152,17 @@ function clearPersonalBankAccountErrors() { function updatePersonalBankAccountInfo(accountData: Partial) { const formattedStreet = getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2); - const parameters = { - phoneNumber: accountData?.phoneNumber, - legalFirstName: accountData?.legalFirstName, - legalLastName: accountData?.legalLastName, - addressStreet: formattedStreet, - addressCity: accountData?.addressCity, - addressState: accountData?.addressState, - addressZip: accountData?.addressZipCode, - addressCountry: accountData?.country, - }; - type AdditionalDataUpdate = {firstName?: string; lastName?: string; addressStreet?: string; addressCity?: string; addressState?: string; addressZipCode?: string; companyPhone?: string}; const bankAccountListUpdates: Record = {}; const bankAccountListRollback: Record = {}; + let bankAccountID: number | undefined; for (const [key, bankAccount] of Object.entries(bankAccountList ?? {})) { if (!isPersonalBankAccountMissingInfo(bankAccount?.accountData)) { continue; } + if (!bankAccountID) { + bankAccountID = bankAccount?.accountData?.bankAccountID; + } const prevData = bankAccount?.accountData?.additionalData; bankAccountListUpdates[key] = { accountData: { @@ -180,7 +173,7 @@ function updatePersonalBankAccountInfo(accountData: Partial { - clearPersonalBankAccount(); - clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - }, []); - const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const exitFlow = () => { Navigation.goBack(ROUTES.SETTINGS_WALLET); @@ -79,11 +74,13 @@ function UpdatePersonalBankAccountPage() { const submitPersonalInfo = () => { const currentAddress = getCurrentAddress(privatePersonalDetails); + const [street1, street2] = getStreetLines(currentAddress?.street); const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; const accountData = { legalFirstName: privatePersonalDetails?.legalFirstName, legalLastName: privatePersonalDetails?.legalLastName, - addressStreet: currentAddress?.street, + addressStreet: street1, + addressStreet2: street2, addressCity: currentAddress?.city, addressState: currentAddress?.state, addressZipCode: currentAddress?.zip, diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index a85b4ab63f90a..8549e18e2a812 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -44,8 +44,9 @@ import {getActiveAdminWorkspaces, getDescriptionForPolicyDomainCard, hasActiveAd import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import {getFirstPageName} from '@pages/settings/Wallet/UpdatePersonalBankAccountPage'; -import {deletePaymentBankAccount, openPersonalBankAccountSetupView, setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; +import {clearPersonalBankAccount, deletePaymentBankAccount, openPersonalBankAccountSetupView, setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; +import {clearDraftValues} from '@userActions/FormActions'; import {close as closeModal} from '@userActions/Modal'; import {clearWalletError, clearWalletTermsError, deletePaymentCard, getPaymentMethods, makeDefaultPaymentMethod as makeDefaultPaymentMethodPaymentMethods} from '@userActions/PaymentMethods'; import {enableCompanyCards} from '@userActions/Policy/Policy'; @@ -174,6 +175,8 @@ function WalletPage() { const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { if (isPersonalBankAccountMissingInfo(accountData)) { + clearPersonalBankAccount(); + clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(privatePersonalDetails))); return; } From 95aad39f61d1a4ca93a8a7835d3c758d5bf6980f Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 01:54:06 +0300 Subject: [PATCH 27/63] fix: resolve TDZ error from parameters used before declaration in optimistic update --- src/libs/actions/BankAccounts.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 530119d62247e..6428c78dabc44 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -167,13 +167,13 @@ function updatePersonalBankAccountInfo(accountData: Partial Date: Fri, 6 Mar 2026 03:12:01 +0300 Subject: [PATCH 28/63] fix: remove incorrect PRIVATE_PERSONAL_DETAILS optimistic update from bank account info submission --- src/libs/BankAccountUtils.ts | 36 ++++++++++++++++++- src/libs/PersonalDetailsUtils.ts | 3 ++ src/libs/actions/BankAccounts.ts | 10 ------ .../Wallet/UpdatePersonalBankAccountPage.tsx | 27 +++++++------- .../settings/Wallet/WalletPage/index.tsx | 4 +-- 5 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 66f8af8d3ef9d..d1463932699c8 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -58,4 +58,38 @@ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): return !hasName || !hasAddress || !hasPhone; } -export {getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, isBankAccountPartiallySetup, doesPolicyHavePartiallySetupBankAccount, isPersonalBankAccountMissingInfo}; +/** + * Returns step numbers (1=name, 2=address, 3=phone) that already have data on the bank account + * and can be skipped in the update flow. + */ +function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry): number[] { + const missingAccount = Object.values(bankAccountList ?? {}).find((bankAccount) => isPersonalBankAccountMissingInfo(bankAccount?.accountData)); + if (!missingAccount) { + return []; + } + + const {additionalData} = missingAccount.accountData ?? {}; + const completedSteps: number[] = []; + + if (!!additionalData?.firstName && !!additionalData?.lastName) { + completedSteps.push(1); + } + if (!!additionalData?.addressStreet && !!additionalData?.addressCity && !!additionalData?.addressState && !!additionalData?.addressZipCode) { + completedSteps.push(2); + } + if (!!additionalData?.companyPhone) { + completedSteps.push(3); + } + + return completedSteps; +} + +export { + getDefaultCompanyWebsite, + getLastFourDigits, + hasPartiallySetupBankAccount, + isBankAccountPartiallySetup, + doesPolicyHavePartiallySetupBankAccount, + isPersonalBankAccountMissingInfo, + getCompletedStepsForBankAccount, +}; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 11bfaf777afeb..f66d5bbfbf15d 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -274,6 +274,9 @@ function formatPiece(piece?: string): string { * @returns formatted street */ function getFormattedStreet(street1 = '', street2 = '') { + if (!street2) { + return street1; + } return `${street1}\n${street2}`; } diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6428c78dabc44..761f95e0cf61d 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -206,7 +206,6 @@ function updatePersonalBankAccountInfo(accountData: Partial): string { - const skippedSteps = getSkippedStepsPersonalInfo(details); - const skipPageNames = new Set(skippedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name)); +function getFirstPageName(bankAccountList?: OnyxEntry): string { + const completedSteps = getCompletedStepsForBankAccount(bankAccountList); + const skipPageNames = new Set(completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name)); const firstPage = PAGE_NAMES.find((name) => !skipPageNames.has(name)); return firstPage ?? PAGE_NAME.LEGAL_NAME; } @@ -63,19 +64,21 @@ function UpdatePersonalBankAccountPage() { const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); const [personalBankAccountDraft] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); - const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const exitFlow = () => { Navigation.goBack(ROUTES.SETTINGS_WALLET); clearPersonalBankAccount(); clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); + clearDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); }; const submitPersonalInfo = () => { const currentAddress = getCurrentAddress(privatePersonalDetails); const [street1, street2] = getStreetLines(currentAddress?.street); const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; + const parsed = parsePhoneNumber(finalPhoneNumber, {regionCode: CONST.COUNTRY.US}); const accountData = { legalFirstName: privatePersonalDetails?.legalFirstName, legalLastName: privatePersonalDetails?.legalLastName, @@ -86,16 +89,16 @@ function UpdatePersonalBankAccountPage() { addressZipCode: currentAddress?.zip, country: currentAddress?.country, ...personalBankAccountDraft, - phoneNumber: formatE164PhoneNumber(finalPhoneNumber, countryCode), + phoneNumber: parsed.number?.significant ?? '', }; updatePersonalBankAccountInfo(accountData); }; - const skippedSteps = getSkippedStepsPersonalInfo(privatePersonalDetails); - const skipPageCandidates = skippedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); + const completedSteps = getCompletedStepsForBankAccount(bankAccountList); + const skipPageCandidates = completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); const skipPages = skipPageCandidates.length >= formPages.length ? [] : skipPageCandidates; - const firstPageName = getFirstPageName(privatePersonalDetails); + const firstPageName = getFirstPageName(bankAccountList); const firstNonSkippedIndex = formPages.findIndex((p) => p.pageName === firstPageName); const {CurrentPage, currentPageName, prevPage, nextPage} = useSubPage({ diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 8549e18e2a812..460d804b899c8 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -80,7 +80,6 @@ function WalletPage() { const [userAccount] = useOnyx(ONYXKEYS.ACCOUNT); const [lastUsedPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); - const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); const isUserValidated = userAccount?.validated ?? false; const {isAccountLocked} = useLockedAccountState(); const {showLockedAccountModal} = useLockedAccountActions(); @@ -177,7 +176,8 @@ function WalletPage() { if (isPersonalBankAccountMissingInfo(accountData)) { clearPersonalBankAccount(); clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(privatePersonalDetails))); + clearDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList))); return; } From 026ef11b57b8cf8a7872b54e4ef864d967f9df43 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 03:37:08 +0300 Subject: [PATCH 29/63] fix: improve type safety, DRY, and optimistic update accuracy --- src/libs/BankAccountUtils.ts | 45 ++++++++++---- src/libs/actions/BankAccounts.ts | 50 +++++++++------ .../PersonalInfo/substeps/AddressStep.tsx | 17 +++-- .../settings/Wallet/PaymentMethodList.tsx | 10 +++ .../Wallet/UpdatePersonalBankAccountPage.tsx | 62 +++++++++++++------ 5 files changed, 126 insertions(+), 58 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index d1463932699c8..1d0f310a1c446 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -31,6 +31,29 @@ function hasPartiallySetupBankAccount(bankAccountList: OnyxEntry isBankAccountPartiallySetup(bankAccount?.accountData?.state)); } +/** + * Step numbers for the personal bank account update flow. + */ +const PERSONAL_INFO_STEP = { + NAME: 1, + ADDRESS: 2, + PHONE: 3, +} as const; + +type AdditionalData = AccountData['additionalData']; + +function hasOwnerName(additionalData: AdditionalData): boolean { + return !!additionalData?.firstName && !!additionalData?.lastName; +} + +function hasOwnerAddress(additionalData: AdditionalData): boolean { + return !!additionalData?.addressStreet && !!additionalData?.addressCity && !!additionalData?.addressState && !!additionalData?.addressZipCode; +} + +function hasOwnerPhone(additionalData: AdditionalData): boolean { + return !!additionalData?.companyPhone; +} + /** * Check if a US personal bank account in OPEN state is missing required personal information. */ @@ -51,16 +74,11 @@ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): } const {additionalData} = accountData; - const hasName = !!additionalData?.firstName && !!additionalData?.lastName; - const hasAddress = !!additionalData?.addressStreet && !!additionalData?.addressCity && !!additionalData?.addressState && !!additionalData?.addressZipCode; - const hasPhone = !!additionalData?.companyPhone; - - return !hasName || !hasAddress || !hasPhone; + return !hasOwnerName(additionalData) || !hasOwnerAddress(additionalData) || !hasOwnerPhone(additionalData); } /** - * Returns step numbers (1=name, 2=address, 3=phone) that already have data on the bank account - * and can be skipped in the update flow. + * Returns step numbers that already have data on the bank account and can be skipped in the update flow. */ function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry): number[] { const missingAccount = Object.values(bankAccountList ?? {}).find((bankAccount) => isPersonalBankAccountMissingInfo(bankAccount?.accountData)); @@ -71,14 +89,14 @@ function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry) { const formattedStreet = getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2); - type AdditionalDataUpdate = {firstName?: string; lastName?: string; addressStreet?: string; addressCity?: string; addressState?: string; addressZipCode?: string; companyPhone?: string}; + // eslint-disable-next-line no-console + console.log('[DEBUG updatePersonalBankAccountInfo] accountData:', JSON.stringify(accountData)); + // eslint-disable-next-line no-console + console.log('[DEBUG updatePersonalBankAccountInfo] formattedStreet:', JSON.stringify(formattedStreet)); + + type AdditionalDataUpdate = Partial>; const bankAccountListUpdates: Record = {}; const bankAccountListRollback: Record = {}; let bankAccountID: number | undefined; @@ -164,17 +171,18 @@ function updatePersonalBankAccountInfo(accountData: Partial ); diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 91cde8fb91513..427cb7357683c 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -439,6 +439,16 @@ function PaymentMethodList({ }; const isMissingPersonalInfo = isPersonalBankAccountMissingInfo(paymentMethod.accountData); + // eslint-disable-next-line no-console + console.log( + '[DEBUG PaymentMethodList] account check:', + JSON.stringify({ + methodID: paymentMethod.methodID, + isMissingPersonalInfo, + additionalData: paymentMethod.accountData?.additionalData, + }), + ); + return { ...paymentMethod, title: paymentMethod.title?.includes(CONST.MASKED_PAN_PREFIX) ? paymentMethod.accountData?.additionalData?.bankName : paymentMethod.title, diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index e1743bdd14be8..d987b45e30ca8 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -9,7 +9,7 @@ import useOnyx from '@hooks/useOnyx'; import useSubPage from '@hooks/useSubPage'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCompletedStepsForBankAccount} from '@libs/BankAccountUtils'; +import {getCompletedStepsForBankAccount, PERSONAL_INFO_STEP} from '@libs/BankAccountUtils'; import {getCurrentAddress, getStreetLines} from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import Navigation from '@navigation/Navigation'; @@ -18,6 +18,7 @@ import {clearDraftValues} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {PersonalBankAccountForm} from '@src/types/form/PersonalBankAccountForm'; import type {BankAccountList} from '@src/types/onyx'; import Address from './InternationalDepositAccount/PersonalInfo/substeps/AddressStep'; import LegalName from './InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep'; @@ -27,6 +28,20 @@ const PAGE_NAME = CONST.UPDATE_PERSONAL_BANK_ACCOUNT.PAGE_NAME; const PAGE_NAMES: string[] = [PAGE_NAME.LEGAL_NAME, PAGE_NAME.ADDRESS, PAGE_NAME.PHONE_NUMBER]; +/** + * Wrapper that enables draft saving on the address form to preserve values across navigation. + */ +function AddressWithDraft({isEditing, onNext, onMove}: SubStepProps) { + return ( +
+ ); +} + /** * Wrapper that delays auto-focus to avoid validation errors during URL-based navigation transitions. */ @@ -43,7 +58,7 @@ function DelayedPhoneNumber({isEditing, onNext, onMove}: SubStepProps) { const formPages = [ {pageName: PAGE_NAME.LEGAL_NAME, component: LegalName}, - {pageName: PAGE_NAME.ADDRESS, component: Address}, + {pageName: PAGE_NAME.ADDRESS, component: AddressWithDraft}, {pageName: PAGE_NAME.PHONE_NUMBER, component: DelayedPhoneNumber}, ]; @@ -67,6 +82,8 @@ function UpdatePersonalBankAccountPage() { const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; + const completedSteps = getCompletedStepsForBankAccount(bankAccountList); + const exitFlow = () => { Navigation.goBack(ROUTES.SETTINGS_WALLET); clearPersonalBankAccount(); @@ -75,26 +92,31 @@ function UpdatePersonalBankAccountPage() { }; const submitPersonalInfo = () => { - const currentAddress = getCurrentAddress(privatePersonalDetails); - const [street1, street2] = getStreetLines(currentAddress?.street); - const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; - const parsed = parsePhoneNumber(finalPhoneNumber, {regionCode: CONST.COUNTRY.US}); - const accountData = { - legalFirstName: privatePersonalDetails?.legalFirstName, - legalLastName: privatePersonalDetails?.legalLastName, - addressStreet: street1, - addressStreet2: street2, - addressCity: currentAddress?.city, - addressState: currentAddress?.state, - addressZipCode: currentAddress?.zip, - country: currentAddress?.country, - ...personalBankAccountDraft, - phoneNumber: parsed.number?.significant ?? '', - }; + const accountData: Partial = {}; + + // Only include data for steps that weren't skipped + if (!completedSteps.includes(PERSONAL_INFO_STEP.NAME)) { + accountData.legalFirstName = personalBankAccountDraft?.legalFirstName ?? privatePersonalDetails?.legalFirstName; + accountData.legalLastName = personalBankAccountDraft?.legalLastName ?? privatePersonalDetails?.legalLastName; + } + if (!completedSteps.includes(PERSONAL_INFO_STEP.ADDRESS)) { + const currentAddress = getCurrentAddress(privatePersonalDetails); + const [street1, street2] = getStreetLines(currentAddress?.street); + accountData.addressStreet = personalBankAccountDraft?.addressStreet ?? street1; + accountData.addressStreet2 = personalBankAccountDraft?.addressStreet2 ?? street2; + accountData.addressCity = personalBankAccountDraft?.addressCity ?? currentAddress?.city; + accountData.addressState = personalBankAccountDraft?.addressState ?? currentAddress?.state; + accountData.addressZipCode = personalBankAccountDraft?.addressZipCode ?? currentAddress?.zip; + accountData.country = personalBankAccountDraft?.country ?? currentAddress?.country; + } + if (!completedSteps.includes(PERSONAL_INFO_STEP.PHONE)) { + const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; + const parsed = parsePhoneNumber(finalPhoneNumber, {regionCode: CONST.COUNTRY.US}); + accountData.phoneNumber = parsed.number?.significant ?? ''; + } + updatePersonalBankAccountInfo(accountData); }; - - const completedSteps = getCompletedStepsForBankAccount(bankAccountList); const skipPageCandidates = completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); const skipPages = skipPageCandidates.length >= formPages.length ? [] : skipPageCandidates; From f9af562f868860889c59bb838e12d5a10e191583 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 03:48:29 +0300 Subject: [PATCH 30/63] fix: scope bank account update to the selected account --- src/libs/BankAccountUtils.ts | 8 +-- src/libs/actions/BankAccounts.ts | 55 ++++++++++--------- .../PersonalInfo/substeps/AddressStep.tsx | 2 - .../settings/Wallet/PaymentMethodListItem.tsx | 34 +++++++----- .../Wallet/UpdatePersonalBankAccountPage.tsx | 13 +++-- .../settings/Wallet/WalletPage/index.tsx | 13 ++++- src/types/onyx/PersonalBankAccount.ts | 3 + 7 files changed, 72 insertions(+), 56 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 1d0f310a1c446..08b2df247092d 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -80,13 +80,13 @@ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): /** * Returns step numbers that already have data on the bank account and can be skipped in the update flow. */ -function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry): number[] { - const missingAccount = Object.values(bankAccountList ?? {}).find((bankAccount) => isPersonalBankAccountMissingInfo(bankAccount?.accountData)); - if (!missingAccount) { +function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry, bankAccountID?: number): number[] { + const bankAccount = bankAccountID ? bankAccountList?.[String(bankAccountID)] : Object.values(bankAccountList ?? {}).find((ba) => isPersonalBankAccountMissingInfo(ba?.accountData)); + if (!bankAccount) { return []; } - const {additionalData} = missingAccount.accountData ?? {}; + const {additionalData} = bankAccount.accountData ?? {}; const completedSteps: number[] = []; if (hasOwnerName(additionalData)) { diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 4e9b023b095de..c5515528e216c 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -151,7 +151,7 @@ function clearPersonalBankAccountErrors() { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); } -function updatePersonalBankAccountInfo(accountData: Partial) { +function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Partial) { const formattedStreet = getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2); // eslint-disable-next-line no-console @@ -160,32 +160,30 @@ function updatePersonalBankAccountInfo(accountData: Partial>; - const bankAccountListUpdates: Record = {}; - const bankAccountListRollback: Record = {}; - let bankAccountID: number | undefined; - for (const [key, bankAccount] of Object.entries(bankAccountList ?? {})) { - if (!isPersonalBankAccountMissingInfo(bankAccount?.accountData)) { - continue; - } - if (!bankAccountID) { - bankAccountID = bankAccount?.accountData?.bankAccountID; - } - const prevData = bankAccount?.accountData?.additionalData; - const additionalDataUpdate: AdditionalDataUpdate = { - ...(accountData?.legalFirstName && {firstName: accountData.legalFirstName}), - ...(accountData?.legalLastName && {lastName: accountData.legalLastName}), - ...(formattedStreet && {addressStreet: formattedStreet}), - ...(accountData?.addressCity && {addressCity: accountData.addressCity}), - ...(accountData?.addressState && {addressState: accountData.addressState}), - ...(accountData?.addressZipCode && {addressZipCode: accountData.addressZipCode}), - ...(accountData?.phoneNumber && {companyPhone: accountData.phoneNumber}), - }; - bankAccountListUpdates[key] = { + + const bankAccountKey = String(bankAccountID); + const prevData = bankAccountList?.[bankAccountKey]?.accountData?.additionalData; + + const additionalDataUpdate: AdditionalDataUpdate = { + ...(accountData?.legalFirstName && {firstName: accountData.legalFirstName}), + ...(accountData?.legalLastName && {lastName: accountData.legalLastName}), + ...(formattedStreet && {addressStreet: formattedStreet}), + ...(accountData?.addressCity && {addressCity: accountData.addressCity}), + ...(accountData?.addressState && {addressState: accountData.addressState}), + ...(accountData?.addressZipCode && {addressZipCode: accountData.addressZipCode}), + ...(accountData?.phoneNumber && {companyPhone: accountData.phoneNumber}), + }; + + const bankAccountListUpdates: Record = { + [bankAccountKey]: { accountData: { additionalData: additionalDataUpdate, }, - }; - bankAccountListRollback[key] = { + }, + }; + + const bankAccountListRollback: Record = { + [bankAccountKey]: { accountData: { additionalData: { firstName: prevData?.firstName, @@ -197,8 +195,8 @@ function updatePersonalBankAccountInfo(accountData: Partial { + if (isNeedingAction) { + return translate('common.actionRequired'); + } + if (item.isCardFrozen) { + return translate('cardPage.frozen'); + } + return shouldShowDefaultBadge ? translate('paymentMethodList.defaultPaymentMethod') : undefined; + }, [isNeedingAction, item.isCardFrozen, shouldShowDefaultBadge, translate]); - let badgeIcon; - if (isNeedingAction) { - badgeIcon = icons.DotIndicator; - } else if (item.isCardFrozen) { - badgeIcon = icons.FreezeCard; - } + const badgeIcon = useMemo(() => { + if (isNeedingAction) { + return icons.DotIndicator; + } + if (item.isCardFrozen) { + return icons.FreezeCard; + } + return undefined; + }, [icons.DotIndicator, icons.FreezeCard, isNeedingAction, item.isCardFrozen]); return ( ): string { - const completedSteps = getCompletedStepsForBankAccount(bankAccountList); +function getFirstPageName(bankAccountList?: OnyxEntry, bankAccountID?: number): string { + const completedSteps = getCompletedStepsForBankAccount(bankAccountList, bankAccountID); const skipPageNames = new Set(completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name)); const firstPage = PAGE_NAMES.find((name) => !skipPageNames.has(name)); return firstPage ?? PAGE_NAME.LEGAL_NAME; @@ -82,7 +82,7 @@ function UpdatePersonalBankAccountPage() { const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; - const completedSteps = getCompletedStepsForBankAccount(bankAccountList); + const completedSteps = getCompletedStepsForBankAccount(bankAccountList, personalBankAccount?.bankAccountID); const exitFlow = () => { Navigation.goBack(ROUTES.SETTINGS_WALLET); @@ -94,7 +94,6 @@ function UpdatePersonalBankAccountPage() { const submitPersonalInfo = () => { const accountData: Partial = {}; - // Only include data for steps that weren't skipped if (!completedSteps.includes(PERSONAL_INFO_STEP.NAME)) { accountData.legalFirstName = personalBankAccountDraft?.legalFirstName ?? privatePersonalDetails?.legalFirstName; accountData.legalLastName = personalBankAccountDraft?.legalLastName ?? privatePersonalDetails?.legalLastName; @@ -115,12 +114,14 @@ function UpdatePersonalBankAccountPage() { accountData.phoneNumber = parsed.number?.significant ?? ''; } - updatePersonalBankAccountInfo(accountData); + if (personalBankAccount?.bankAccountID) { + updatePersonalBankAccountInfo(personalBankAccount.bankAccountID, accountData); + } }; const skipPageCandidates = completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); const skipPages = skipPageCandidates.length >= formPages.length ? [] : skipPageCandidates; - const firstPageName = getFirstPageName(bankAccountList); + const firstPageName = getFirstPageName(bankAccountList, personalBankAccount?.bankAccountID); const firstNonSkippedIndex = formPages.findIndex((p) => p.pageName === firstPageName); const {CurrentPage, currentPageName, prevPage, nextPage} = useSubPage({ diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 460d804b899c8..0774d726c3c19 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -44,7 +44,13 @@ import {getActiveAdminWorkspaces, getDescriptionForPolicyDomainCard, hasActiveAd import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import {getFirstPageName} from '@pages/settings/Wallet/UpdatePersonalBankAccountPage'; -import {clearPersonalBankAccount, deletePaymentBankAccount, openPersonalBankAccountSetupView, setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; +import { + clearPersonalBankAccount, + deletePaymentBankAccount, + openPersonalBankAccountSetupView, + setPersonalBankAccountContinueKYCOnSuccess, + setPersonalBankAccountID, +} from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; import {clearDraftValues} from '@userActions/FormActions'; import {close as closeModal} from '@userActions/Modal'; @@ -173,11 +179,12 @@ function WalletPage() { }; const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { - if (isPersonalBankAccountMissingInfo(accountData)) { + if (isPersonalBankAccountMissingInfo(accountData) && accountData?.bankAccountID) { clearPersonalBankAccount(); + setPersonalBankAccountID(accountData.bankAccountID); clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); clearDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList))); + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); return; } diff --git a/src/types/onyx/PersonalBankAccount.ts b/src/types/onyx/PersonalBankAccount.ts index 3625ea1e8c814..4f41824122477 100644 --- a/src/types/onyx/PersonalBankAccount.ts +++ b/src/types/onyx/PersonalBankAccount.ts @@ -29,6 +29,9 @@ type PersonalBankAccount = { /** If set, continue with the KYC flow after adding a PBA. This specifies the fallback route to use. */ onSuccessFallbackRoute?: Route; + + /** The bank account ID being updated in the personal info update flow */ + bankAccountID?: number; }; export default PersonalBankAccount; From bf117713150d88ba7a53ce2156b793453128c235 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 03:52:50 +0300 Subject: [PATCH 31/63] fix: remove debug loggers and unused import --- src/libs/actions/BankAccounts.ts | 10 ---------- src/pages/settings/Wallet/PaymentMethodList.tsx | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index c5515528e216c..03559acd87167 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -24,7 +24,6 @@ import type AskForCorpaySignerInformationParams from '@libs/API/parameters/AskFo import type {SaveCorpayOnboardingCompanyDetails} from '@libs/API/parameters/SaveCorpayOnboardingCompanyDetailsParams'; import type SaveCorpayOnboardingDirectorInformationParams from '@libs/API/parameters/SaveCorpayOnboardingDirectorInformationParams'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; -import {isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {MemberForList} from '@libs/OptionsListUtils'; @@ -154,11 +153,6 @@ function clearPersonalBankAccountErrors() { function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Partial) { const formattedStreet = getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2); - // eslint-disable-next-line no-console - console.log('[DEBUG updatePersonalBankAccountInfo] accountData:', JSON.stringify(accountData)); - // eslint-disable-next-line no-console - console.log('[DEBUG updatePersonalBankAccountInfo] formattedStreet:', JSON.stringify(formattedStreet)); - type AdditionalDataUpdate = Partial>; const bankAccountKey = String(bankAccountID); @@ -284,10 +278,6 @@ function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Parti ], }; - // eslint-disable-next-line no-console - console.log('[DEBUG updatePersonalBankAccountInfo] optimistic bankAccountListUpdates:', JSON.stringify(bankAccountListUpdates)); - // eslint-disable-next-line no-console - console.log('[DEBUG updatePersonalBankAccountInfo] parameters:', JSON.stringify(parameters)); API.write(WRITE_COMMANDS.UPDATE_PERSONAL_BANK_ACCOUNT_INFO, parameters, onyxData); } diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 427cb7357683c..91cde8fb91513 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -439,16 +439,6 @@ function PaymentMethodList({ }; const isMissingPersonalInfo = isPersonalBankAccountMissingInfo(paymentMethod.accountData); - // eslint-disable-next-line no-console - console.log( - '[DEBUG PaymentMethodList] account check:', - JSON.stringify({ - methodID: paymentMethod.methodID, - isMissingPersonalInfo, - additionalData: paymentMethod.accountData?.additionalData, - }), - ); - return { ...paymentMethod, title: paymentMethod.title?.includes(CONST.MASKED_PAN_PREFIX) ? paymentMethod.accountData?.additionalData?.bankName : paymentMethod.title, From 4220e7944387fbb63155744c606586c407d18959 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 04:10:15 +0300 Subject: [PATCH 32/63] fix: remove dead fallback in getCompletedStepsForBankAccount and add unit tests --- src/libs/BankAccountUtils.ts | 4 +- .../Wallet/UpdatePersonalBankAccountPage.tsx | 5 +- tests/unit/BankAccountUtilsTest.ts | 103 +++++++++++++++++- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 08b2df247092d..4516e5dee9f20 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -80,8 +80,8 @@ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): /** * Returns step numbers that already have data on the bank account and can be skipped in the update flow. */ -function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry, bankAccountID?: number): number[] { - const bankAccount = bankAccountID ? bankAccountList?.[String(bankAccountID)] : Object.values(bankAccountList ?? {}).find((ba) => isPersonalBankAccountMissingInfo(ba?.accountData)); +function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry, bankAccountID: number): number[] { + const bankAccount = bankAccountList?.[String(bankAccountID)]; if (!bankAccount) { return []; } diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index cedccbd2547a3..230068ce0c511 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -66,7 +66,7 @@ const formPages = [ * Returns the first non-skipped page name for the update flow based on the bank account's existing data. */ function getFirstPageName(bankAccountList?: OnyxEntry, bankAccountID?: number): string { - const completedSteps = getCompletedStepsForBankAccount(bankAccountList, bankAccountID); + const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; const skipPageNames = new Set(completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name)); const firstPage = PAGE_NAMES.find((name) => !skipPageNames.has(name)); return firstPage ?? PAGE_NAME.LEGAL_NAME; @@ -82,7 +82,8 @@ function UpdatePersonalBankAccountPage() { const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; - const completedSteps = getCompletedStepsForBankAccount(bankAccountList, personalBankAccount?.bankAccountID); + const bankAccountID = personalBankAccount?.bankAccountID; + const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; const exitFlow = () => { Navigation.goBack(ROUTES.SETTINGS_WALLET); diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 60b99cda4e23b..557978bdee75d 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -1,4 +1,12 @@ -import {getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, isBankAccountPartiallySetup, isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; +import { + getCompletedStepsForBankAccount, + getDefaultCompanyWebsite, + getLastFourDigits, + hasPartiallySetupBankAccount, + isBankAccountPartiallySetup, + isPersonalBankAccountMissingInfo, + PERSONAL_INFO_STEP, +} from '@libs/BankAccountUtils'; import CONST from '@src/CONST'; import type {Account, BankAccountList, Session} from '@src/types/onyx'; import type AccountData from '@src/types/onyx/AccountData'; @@ -225,4 +233,97 @@ describe('BankAccountUtils', () => { expect(getDefaultCompanyWebsite(undefined, account)).toBe('https://www.'); }); }); + + describe('getCompletedStepsForBankAccount', () => { + const bankAccountID = 12345; + const bankAccountKey = String(bankAccountID); + + const fullAdditionalData = { + firstName: 'John', + lastName: 'Doe', + addressStreet: '123 Main St', + addressCity: 'New York', + addressState: 'NY', + addressZipCode: '10001', + companyPhone: '+15551234567', + }; + + it('returns all steps when all data is present', () => { + const bankAccountList = { + [bankAccountKey]: {accountData: {additionalData: fullAdditionalData}, bankCurrency: 'USD', bankCountry: 'US'}, + } as unknown as BankAccountList; + const result = getCompletedStepsForBankAccount(bankAccountList, bankAccountID); + expect(result).toEqual([PERSONAL_INFO_STEP.NAME, PERSONAL_INFO_STEP.ADDRESS, PERSONAL_INFO_STEP.PHONE]); + }); + + it('returns empty array when bank account is not found', () => { + const bankAccountList = {} as BankAccountList; + expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([]); + }); + + it('returns empty array when bankAccountList is undefined', () => { + expect(getCompletedStepsForBankAccount(undefined, bankAccountID)).toEqual([]); + }); + + it('returns only NAME step when only name fields are present', () => { + const bankAccountList = { + [bankAccountKey]: {accountData: {additionalData: {firstName: 'John', lastName: 'Doe'}}, bankCurrency: 'USD', bankCountry: 'US'}, + } as unknown as BankAccountList; + expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.NAME]); + }); + + it('returns only ADDRESS step when only address fields are present', () => { + const bankAccountList = { + [bankAccountKey]: { + accountData: {additionalData: {addressStreet: '123 Main St', addressCity: 'New York', addressState: 'NY', addressZipCode: '10001'}}, + bankCurrency: 'USD', + bankCountry: 'US', + }, + } as unknown as BankAccountList; + expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.ADDRESS]); + }); + + it('returns only PHONE step when only phone is present', () => { + const bankAccountList = { + [bankAccountKey]: {accountData: {additionalData: {companyPhone: '+15551234567'}}, bankCurrency: 'USD', bankCountry: 'US'}, + } as unknown as BankAccountList; + expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.PHONE]); + }); + + it('returns empty array when accountData has no additionalData', () => { + const bankAccountList = { + [bankAccountKey]: {accountData: {}, bankCurrency: 'USD', bankCountry: 'US'}, + } as unknown as BankAccountList; + expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([]); + }); + + it('does not include NAME when only firstName is present (lastName missing)', () => { + const bankAccountList = { + [bankAccountKey]: {accountData: {additionalData: {firstName: 'John'}}, bankCurrency: 'USD', bankCountry: 'US'}, + } as unknown as BankAccountList; + expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([]); + }); + + it('returns multiple steps when some groups are complete', () => { + const bankAccountList = { + [bankAccountKey]: { + accountData: {additionalData: {firstName: 'John', lastName: 'Doe', companyPhone: '+15551234567'}}, + bankCurrency: 'USD', + bankCountry: 'US', + }, + } as unknown as BankAccountList; + expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([PERSONAL_INFO_STEP.NAME, PERSONAL_INFO_STEP.PHONE]); + }); + + it('does not include ADDRESS when one address field is missing', () => { + const bankAccountList = { + [bankAccountKey]: { + accountData: {additionalData: {addressStreet: '123 Main St', addressCity: 'New York', addressState: 'NY'}}, + bankCurrency: 'USD', + bankCountry: 'US', + }, + } as unknown as BankAccountList; + expect(getCompletedStepsForBankAccount(bankAccountList, bankAccountID)).toEqual([]); + }); + }); }); From a00ab2a0622d1203b9755c52c52fa0410c1d3863 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 04:14:04 +0300 Subject: [PATCH 33/63] fix: redirect to Wallet when bankAccountID is missing on direct URL access --- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 230068ce0c511..6931345f8d761 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -83,6 +83,13 @@ function UpdatePersonalBankAccountPage() { const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const bankAccountID = personalBankAccount?.bankAccountID; + + useEffect(() => { + if (!bankAccountID && !shouldShowSuccess) { + Navigation.goBack(ROUTES.SETTINGS_WALLET); + } + }, [bankAccountID, shouldShowSuccess]); + const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; const exitFlow = () => { From 1100f2073f11db3d4297e9796699880ff3713504 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 01:35:28 +0300 Subject: [PATCH 34/63] fix: replace useEffect redirect with early return guard in submitPersonalInfo --- .../Wallet/UpdatePersonalBankAccountPage.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 6931345f8d761..df96bb22d0354 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -84,12 +84,6 @@ function UpdatePersonalBankAccountPage() { const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const bankAccountID = personalBankAccount?.bankAccountID; - useEffect(() => { - if (!bankAccountID && !shouldShowSuccess) { - Navigation.goBack(ROUTES.SETTINGS_WALLET); - } - }, [bankAccountID, shouldShowSuccess]); - const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; const exitFlow = () => { @@ -100,6 +94,10 @@ function UpdatePersonalBankAccountPage() { }; const submitPersonalInfo = () => { + if (!personalBankAccount?.bankAccountID) { + return; + } + const accountData: Partial = {}; if (!completedSteps.includes(PERSONAL_INFO_STEP.NAME)) { @@ -122,9 +120,7 @@ function UpdatePersonalBankAccountPage() { accountData.phoneNumber = parsed.number?.significant ?? ''; } - if (personalBankAccount?.bankAccountID) { - updatePersonalBankAccountInfo(personalBankAccount.bankAccountID, accountData); - } + updatePersonalBankAccountInfo(personalBankAccount.bankAccountID, accountData); }; const skipPageCandidates = completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); const skipPages = skipPageCandidates.length >= formPages.length ? [] : skipPageCandidates; From 9dcfbc3d408a3a638dfbc9e78b985f66db4522f8 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 03:38:58 +0300 Subject: [PATCH 35/63] fix: display BE errors above submit button, hide country selector, fix address autofill race condition --- src/components/AddressForm.tsx | 28 +++++++++++------- .../UpdatePersonalBankAccountInfoParams.ts | 1 - src/libs/actions/BankAccounts.ts | 23 ++++++++++++++- .../PersonalInfo/substeps/AddressStep.tsx | 6 +++- .../Wallet/UpdatePersonalBankAccountPage.tsx | 16 +++++----- .../settings/Wallet/WalletPage/index.tsx | 29 ++++++++++++++++--- 6 files changed, 79 insertions(+), 24 deletions(-) diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 4d528067be0f5..3218a7f227cee 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -55,6 +55,9 @@ type AddressFormProps = { /** A unique Onyx key identifying the form */ formID: typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM; + + /** Whether to hide the country selector (e.g. when country cannot be changed) */ + shouldHideCountrySelector?: boolean; }; function AddressForm({ @@ -69,6 +72,7 @@ function AddressForm({ street2 = '', submitButtonText = '', zip = '', + shouldHideCountrySelector = false, }: AddressFormProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -192,16 +196,20 @@ function AddressForm({ autoComplete="address-line2" /> - - - - + {!shouldHideCountrySelector && ( + <> + + + + + + )} {isUSAForm ? ( ); diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index df96bb22d0354..2a937e3c896e7 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -29,7 +29,8 @@ const PAGE_NAME = CONST.UPDATE_PERSONAL_BANK_ACCOUNT.PAGE_NAME; const PAGE_NAMES: string[] = [PAGE_NAME.LEGAL_NAME, PAGE_NAME.ADDRESS, PAGE_NAME.PHONE_NUMBER]; /** - * Wrapper that enables draft saving on the address form to preserve values across navigation. + * Wrapper that enables draft saving on the address form and hides the country selector + * since the update flow only supports US bank accounts. */ function AddressWithDraft({isEditing, onNext, onMove}: SubStepProps) { return ( @@ -38,6 +39,7 @@ function AddressWithDraft({isEditing, onNext, onMove}: SubStepProps) { onNext={onNext} onMove={onMove} shouldSaveDraft + shouldHideCountrySelector /> ); } @@ -78,6 +80,7 @@ function UpdatePersonalBankAccountPage() { const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); const [personalBankAccountDraft] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + const [homeAddressDraft] = useOnyx(ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT); const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); @@ -107,12 +110,11 @@ function UpdatePersonalBankAccountPage() { if (!completedSteps.includes(PERSONAL_INFO_STEP.ADDRESS)) { const currentAddress = getCurrentAddress(privatePersonalDetails); const [street1, street2] = getStreetLines(currentAddress?.street); - accountData.addressStreet = personalBankAccountDraft?.addressStreet ?? street1; - accountData.addressStreet2 = personalBankAccountDraft?.addressStreet2 ?? street2; - accountData.addressCity = personalBankAccountDraft?.addressCity ?? currentAddress?.city; - accountData.addressState = personalBankAccountDraft?.addressState ?? currentAddress?.state; - accountData.addressZipCode = personalBankAccountDraft?.addressZipCode ?? currentAddress?.zip; - accountData.country = personalBankAccountDraft?.country ?? currentAddress?.country; + accountData.addressStreet = personalBankAccountDraft?.addressStreet ?? homeAddressDraft?.addressLine1 ?? street1; + accountData.addressStreet2 = personalBankAccountDraft?.addressStreet2 ?? homeAddressDraft?.addressLine2 ?? street2; + accountData.addressCity = personalBankAccountDraft?.addressCity ?? homeAddressDraft?.city ?? currentAddress?.city; + accountData.addressState = personalBankAccountDraft?.addressState ?? homeAddressDraft?.state ?? currentAddress?.state; + accountData.addressZipCode = personalBankAccountDraft?.addressZipCode ?? homeAddressDraft?.zipPostCode ?? currentAddress?.zip; } if (!completedSteps.includes(PERSONAL_INFO_STEP.PHONE)) { const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 0774d726c3c19..0af94fddefa9c 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -52,7 +52,7 @@ import { setPersonalBankAccountID, } from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; -import {clearDraftValues} from '@userActions/FormActions'; +import {clearDraftValues, setDraftValues} from '@userActions/FormActions'; import {close as closeModal} from '@userActions/Modal'; import {clearWalletError, clearWalletTermsError, deletePaymentCard, getPaymentMethods, makeDefaultPaymentMethod as makeDefaultPaymentMethodPaymentMethods} from '@userActions/PaymentMethods'; import {enableCompanyCards} from '@userActions/Policy/Policy'; @@ -180,11 +180,32 @@ function WalletPage() { const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { if (isPersonalBankAccountMissingInfo(accountData) && accountData?.bankAccountID) { + const additionalData = accountData?.additionalData; + const [street1 = '', street2 = ''] = additionalData?.addressStreet?.split('\n') ?? []; clearPersonalBankAccount(); setPersonalBankAccountID(accountData.bankAccountID); - clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - clearDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); + Promise.all([ + setDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, { + legalFirstName: additionalData?.firstName ?? '', + legalLastName: additionalData?.lastName ?? '', + addressStreet: street1, + addressStreet2: street2, + addressCity: additionalData?.addressCity ?? '', + addressState: additionalData?.addressState ?? '', + addressZipCode: additionalData?.addressZipCode ?? '', + phoneNumber: additionalData?.companyPhone ?? '', + }), + setDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM, { + addressLine1: street1, + addressLine2: street2, + city: additionalData?.addressCity ?? '', + state: additionalData?.addressState ?? '', + zipPostCode: additionalData?.addressZipCode ?? '', + country: CONST.COUNTRY.US, + }), + ]).then(() => { + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); + }); return; } From e03028485c6a6c4e52c208e6a015bdbc0f030ec8 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 03:43:29 +0300 Subject: [PATCH 36/63] refactor: convert Promise.all().then() to async/await in onBankAccountRowPressed --- src/pages/settings/Wallet/WalletPage/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 0af94fddefa9c..f5b800b3a2fd7 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -178,13 +178,13 @@ function WalletPage() { } }; - const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { + const onBankAccountRowPressed = async ({accountData}: PaymentMethodPressHandlerParams) => { if (isPersonalBankAccountMissingInfo(accountData) && accountData?.bankAccountID) { const additionalData = accountData?.additionalData; const [street1 = '', street2 = ''] = additionalData?.addressStreet?.split('\n') ?? []; clearPersonalBankAccount(); setPersonalBankAccountID(accountData.bankAccountID); - Promise.all([ + await Promise.all([ setDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, { legalFirstName: additionalData?.firstName ?? '', legalLastName: additionalData?.lastName ?? '', @@ -203,9 +203,8 @@ function WalletPage() { zipPostCode: additionalData?.addressZipCode ?? '', country: CONST.COUNTRY.US, }), - ]).then(() => { - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); - }); + ]); + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); return; } From fd27b68c9300b4768e457b5866866b3bd9fb9c7e Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 03:50:09 +0300 Subject: [PATCH 37/63] refactor: extract getPageNamesForCompletedSteps to deduplicate step-to-page mapping --- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 2a937e3c896e7..5f5cb94b40900 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -64,12 +64,19 @@ const formPages = [ {pageName: PAGE_NAME.PHONE_NUMBER, component: DelayedPhoneNumber}, ]; +/** + * Maps completed step numbers to their corresponding page names. + */ +function getPageNamesForCompletedSteps(completedSteps: number[]): string[] { + return completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); +} + /** * Returns the first non-skipped page name for the update flow based on the bank account's existing data. */ function getFirstPageName(bankAccountList?: OnyxEntry, bankAccountID?: number): string { const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; - const skipPageNames = new Set(completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name)); + const skipPageNames = new Set(getPageNamesForCompletedSteps(completedSteps)); const firstPage = PAGE_NAMES.find((name) => !skipPageNames.has(name)); return firstPage ?? PAGE_NAME.LEGAL_NAME; } @@ -124,7 +131,7 @@ function UpdatePersonalBankAccountPage() { updatePersonalBankAccountInfo(personalBankAccount.bankAccountID, accountData); }; - const skipPageCandidates = completedSteps.map((step) => PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); + const skipPageCandidates = getPageNamesForCompletedSteps(completedSteps); const skipPages = skipPageCandidates.length >= formPages.length ? [] : skipPageCandidates; const firstPageName = getFirstPageName(bankAccountList, personalBankAccount?.bankAccountID); From 80d6ba42b8c2e4d208a6868cacb2775ed31ea3f1 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 04:06:14 +0300 Subject: [PATCH 38/63] fix: display API errors on all steps, clear drafts on back, remove unused import --- .../Wallet/UpdatePersonalBankAccountPage.tsx | 22 ++++++++++++++++--- .../settings/Wallet/WalletPage/index.tsx | 11 +++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 5f5cb94b40900..b8b9fb40684c0 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; +import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -10,10 +11,11 @@ import useSubPage from '@hooks/useSubPage'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCompletedStepsForBankAccount, PERSONAL_INFO_STEP} from '@libs/BankAccountUtils'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; import {getCurrentAddress, getStreetLines} from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import Navigation from '@navigation/Navigation'; -import {clearPersonalBankAccount, updatePersonalBankAccountInfo} from '@userActions/BankAccounts'; +import {clearPersonalBankAccount, clearPersonalBankAccountErrors, updatePersonalBankAccountInfo} from '@userActions/BankAccounts'; import {clearDraftValues} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -93,6 +95,7 @@ function UpdatePersonalBankAccountPage() { const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const bankAccountID = personalBankAccount?.bankAccountID; + const errorMessage = getLatestErrorMessage(personalBankAccount ?? {}); const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; @@ -146,13 +149,19 @@ function UpdatePersonalBankAccountPage() { }); const handleBackButtonPress = () => { + clearPersonalBankAccountErrors(); if (currentPageName === firstPageName) { - Navigation.goBack(); + exitFlow(); return; } prevPage(); }; + const handleNextPage = () => { + clearPersonalBankAccountErrors(); + nextPage(); + }; + if (shouldShowSuccess) { return ( {}} /> + {!!errorMessage && ( + + )} ); } diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index f5b800b3a2fd7..89a0c75a49240 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -52,7 +52,7 @@ import { setPersonalBankAccountID, } from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; -import {clearDraftValues, setDraftValues} from '@userActions/FormActions'; +import {setDraftValues} from '@userActions/FormActions'; import {close as closeModal} from '@userActions/Modal'; import {clearWalletError, clearWalletTermsError, deletePaymentCard, getPaymentMethods, makeDefaultPaymentMethod as makeDefaultPaymentMethodPaymentMethods} from '@userActions/PaymentMethods'; import {enableCompanyCards} from '@userActions/Policy/Policy'; @@ -178,13 +178,13 @@ function WalletPage() { } }; - const onBankAccountRowPressed = async ({accountData}: PaymentMethodPressHandlerParams) => { + const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { if (isPersonalBankAccountMissingInfo(accountData) && accountData?.bankAccountID) { const additionalData = accountData?.additionalData; const [street1 = '', street2 = ''] = additionalData?.addressStreet?.split('\n') ?? []; clearPersonalBankAccount(); setPersonalBankAccountID(accountData.bankAccountID); - await Promise.all([ + Promise.all([ setDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, { legalFirstName: additionalData?.firstName ?? '', legalLastName: additionalData?.lastName ?? '', @@ -203,8 +203,9 @@ function WalletPage() { zipPostCode: additionalData?.addressZipCode ?? '', country: CONST.COUNTRY.US, }), - ]); - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); + ]).then(() => { + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); + }); return; } From 0ff6b9b1430f46984da1abd1aa4ecf46d3e4b948 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 04:17:08 +0300 Subject: [PATCH 39/63] fix: remove form-level error from failureData --- src/libs/actions/BankAccounts.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index ad7725583bd4c..6258220d1e91e 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -285,13 +285,6 @@ function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Parti errors: getMicroSecondOnyxErrorWithTranslationKey('addPersonalBankAccount.updatePersonalInfoFailure'), }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, - value: { - errors: getMicroSecondOnyxErrorWithTranslationKey('addPersonalBankAccount.updatePersonalInfoFailure'), - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.BANK_ACCOUNT_LIST, From 9796e9df800bc2ae3986f3da193cb4f24d894f65 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 04:31:17 +0300 Subject: [PATCH 40/63] fix: prevent empty draft strings from blocking address fallback --- src/libs/BankAccountUtils.ts | 5 +++++ src/pages/settings/InitialSettingsPage.tsx | 4 ++-- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 11 ++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 4516e5dee9f20..e73a0aa09e364 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -102,10 +102,15 @@ function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry): boolean { + return Object.values(bankAccountList ?? {}).some((bankAccount) => isPersonalBankAccountMissingInfo(bankAccount?.accountData)); +} + export { getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, + hasPersonalBankAccountMissingInfo, isBankAccountPartiallySetup, doesPolicyHavePartiallySetupBankAccount, isPersonalBankAccountMissingInfo, diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index ca950ea665e1f..c8e549348a4ff 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -38,7 +38,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; import {closeReactNativeApp} from '@libs/actions/HybridApp'; -import {hasPartiallySetupBankAccount} from '@libs/BankAccountUtils'; +import {hasPartiallySetupBankAccount, hasPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import {hasPendingExpensifyCardAction} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import useIsSidebarRouteActive from '@libs/Navigation/helpers/useIsSidebarRouteActive'; @@ -174,7 +174,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr shouldShowRBRForPersonalCard ) { walletBrickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; - } else if (hasPartiallySetupBankAccount(bankAccountList) || hasPendingCardAction) { + } else if (hasPartiallySetupBankAccount(bankAccountList) || hasPersonalBankAccountMissingInfo(bankAccountList) || hasPendingCardAction) { walletBrickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; } diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index b8b9fb40684c0..87e720298c2f7 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -120,11 +120,12 @@ function UpdatePersonalBankAccountPage() { if (!completedSteps.includes(PERSONAL_INFO_STEP.ADDRESS)) { const currentAddress = getCurrentAddress(privatePersonalDetails); const [street1, street2] = getStreetLines(currentAddress?.street); - accountData.addressStreet = personalBankAccountDraft?.addressStreet ?? homeAddressDraft?.addressLine1 ?? street1; - accountData.addressStreet2 = personalBankAccountDraft?.addressStreet2 ?? homeAddressDraft?.addressLine2 ?? street2; - accountData.addressCity = personalBankAccountDraft?.addressCity ?? homeAddressDraft?.city ?? currentAddress?.city; - accountData.addressState = personalBankAccountDraft?.addressState ?? homeAddressDraft?.state ?? currentAddress?.state; - accountData.addressZipCode = personalBankAccountDraft?.addressZipCode ?? homeAddressDraft?.zipPostCode ?? currentAddress?.zip; + accountData.addressStreet = (personalBankAccountDraft?.addressStreet ? personalBankAccountDraft.addressStreet : undefined) ?? homeAddressDraft?.addressLine1 ?? street1; + accountData.addressStreet2 = (personalBankAccountDraft?.addressStreet2 ? personalBankAccountDraft.addressStreet2 : undefined) ?? homeAddressDraft?.addressLine2 ?? street2; + accountData.addressCity = (personalBankAccountDraft?.addressCity ? personalBankAccountDraft.addressCity : undefined) ?? homeAddressDraft?.city ?? currentAddress?.city; + accountData.addressState = (personalBankAccountDraft?.addressState ? personalBankAccountDraft.addressState : undefined) ?? homeAddressDraft?.state ?? currentAddress?.state; + accountData.addressZipCode = + (personalBankAccountDraft?.addressZipCode ? personalBankAccountDraft.addressZipCode : undefined) ?? homeAddressDraft?.zipPostCode ?? currentAddress?.zip; } if (!completedSteps.includes(PERSONAL_INFO_STEP.PHONE)) { const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; From f1ca43eb8896fad58b428f092de88d26a0b77377 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 04:52:08 +0300 Subject: [PATCH 41/63] fix: use undefined instead of empty strings in draft seeding to preserve profile defaults --- .../settings/Wallet/WalletPage/index.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 89a0c75a49240..1cad54e2fc8ab 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -181,26 +181,26 @@ function WalletPage() { const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { if (isPersonalBankAccountMissingInfo(accountData) && accountData?.bankAccountID) { const additionalData = accountData?.additionalData; - const [street1 = '', street2 = ''] = additionalData?.addressStreet?.split('\n') ?? []; + const [street1, street2] = additionalData?.addressStreet?.split('\n') ?? []; clearPersonalBankAccount(); setPersonalBankAccountID(accountData.bankAccountID); Promise.all([ setDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, { - legalFirstName: additionalData?.firstName ?? '', - legalLastName: additionalData?.lastName ?? '', - addressStreet: street1, - addressStreet2: street2, - addressCity: additionalData?.addressCity ?? '', - addressState: additionalData?.addressState ?? '', - addressZipCode: additionalData?.addressZipCode ?? '', - phoneNumber: additionalData?.companyPhone ?? '', + legalFirstName: additionalData?.firstName, + legalLastName: additionalData?.lastName, + addressStreet: street1 || undefined, + addressStreet2: street2 || undefined, + addressCity: additionalData?.addressCity, + addressState: additionalData?.addressState, + addressZipCode: additionalData?.addressZipCode, + phoneNumber: additionalData?.companyPhone, }), setDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM, { - addressLine1: street1, - addressLine2: street2, - city: additionalData?.addressCity ?? '', - state: additionalData?.addressState ?? '', - zipPostCode: additionalData?.addressZipCode ?? '', + addressLine1: street1 || undefined, + addressLine2: street2 || undefined, + city: additionalData?.addressCity, + state: additionalData?.addressState, + zipPostCode: additionalData?.addressZipCode, country: CONST.COUNTRY.US, }), ]).then(() => { From cdc344ac2beb162cfdc6d8f80567a1425b936037 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 05:04:57 +0300 Subject: [PATCH 42/63] fix: keep street2 as empty string in draft --- src/pages/settings/Wallet/WalletPage/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 1cad54e2fc8ab..c57a4e6ba22f5 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -189,7 +189,7 @@ function WalletPage() { legalFirstName: additionalData?.firstName, legalLastName: additionalData?.lastName, addressStreet: street1 || undefined, - addressStreet2: street2 || undefined, + addressStreet2: street2 ?? '', addressCity: additionalData?.addressCity, addressState: additionalData?.addressState, addressZipCode: additionalData?.addressZipCode, @@ -197,7 +197,7 @@ function WalletPage() { }), setDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM, { addressLine1: street1 || undefined, - addressLine2: street2 || undefined, + addressLine2: street2 ?? '', city: additionalData?.addressCity, state: additionalData?.addressState, zipPostCode: additionalData?.addressZipCode, From 93d9ae1e6b944999340f72f1a9e9c3b31007775f Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 05:14:31 +0300 Subject: [PATCH 43/63] fix: clear HOME_ADDRESS_FORM draft on flow entry to prevent stale address data --- src/pages/settings/Wallet/WalletPage/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index c57a4e6ba22f5..43d323e74c344 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -52,7 +52,7 @@ import { setPersonalBankAccountID, } from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; -import {setDraftValues} from '@userActions/FormActions'; +import {clearDraftValues, setDraftValues} from '@userActions/FormActions'; import {close as closeModal} from '@userActions/Modal'; import {clearWalletError, clearWalletTermsError, deletePaymentCard, getPaymentMethods, makeDefaultPaymentMethod as makeDefaultPaymentMethodPaymentMethods} from '@userActions/PaymentMethods'; import {enableCompanyCards} from '@userActions/Policy/Policy'; @@ -183,6 +183,7 @@ function WalletPage() { const additionalData = accountData?.additionalData; const [street1, street2] = additionalData?.addressStreet?.split('\n') ?? []; clearPersonalBankAccount(); + clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); setPersonalBankAccountID(accountData.bankAccountID); Promise.all([ setDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, { From 51a41cd63fb8eea91569245d36ada246f9b35ac1 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 11:57:31 +0300 Subject: [PATCH 44/63] fix: revert setPersonalBankAccountID into Promise.all due to lint error --- .../Wallet/UpdatePersonalBankAccountPage.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 87e720298c2f7..39eae9fd50cfe 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -99,15 +99,20 @@ function UpdatePersonalBankAccountPage() { const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; + useEffect(() => { + return () => { + clearPersonalBankAccount(); + clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); + clearDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); + }; + }, []); + const exitFlow = () => { Navigation.goBack(ROUTES.SETTINGS_WALLET); - clearPersonalBankAccount(); - clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - clearDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); }; const submitPersonalInfo = () => { - if (!personalBankAccount?.bankAccountID) { + if (!personalBankAccount?.bankAccountID || personalBankAccount?.isLoading) { return; } From e55959a689fa5647be1cebff5ea2572b49e076ea Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 12:35:24 +0300 Subject: [PATCH 45/63] fix: eliminate Onyx set/merge race by using atomic reset for bank account ID --- src/libs/actions/BankAccounts.ts | 11 +++++++++++ src/pages/settings/Wallet/WalletPage/index.tsx | 11 ++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6258220d1e91e..6a74b3265b521 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -314,6 +314,16 @@ function setPersonalBankAccountID(bankAccountID: number) { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {bankAccountID}); } +/** + * Atomically resets personal bank account state and sets the bankAccountID using a single Onyx.set, + * avoiding the race condition between set(null) and merge({bankAccountID}) on the same key. + */ +function resetPersonalBankAccountForUpdate(bankAccountID: number) { + clearPlaid(); + Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {bankAccountID}); + Onyx.set(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, null); +} + function clearOnfidoToken() { Onyx.merge(ONYXKEYS.ONFIDO_TOKEN, ''); Onyx.merge(ONYXKEYS.ONFIDO_APPLICANT_ID, ''); @@ -1650,6 +1660,7 @@ export { addPersonalBankAccount, clearOnfidoToken, clearPersonalBankAccount, + resetPersonalBankAccountForUpdate, setPersonalBankAccountID, setPlaidEvent, openPlaidView, diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 43d323e74c344..2cc6e6a9a947c 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -44,13 +44,7 @@ import {getActiveAdminWorkspaces, getDescriptionForPolicyDomainCard, hasActiveAd import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import {getFirstPageName} from '@pages/settings/Wallet/UpdatePersonalBankAccountPage'; -import { - clearPersonalBankAccount, - deletePaymentBankAccount, - openPersonalBankAccountSetupView, - setPersonalBankAccountContinueKYCOnSuccess, - setPersonalBankAccountID, -} from '@userActions/BankAccounts'; +import {deletePaymentBankAccount, openPersonalBankAccountSetupView, resetPersonalBankAccountForUpdate, setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; import {clearDraftValues, setDraftValues} from '@userActions/FormActions'; import {close as closeModal} from '@userActions/Modal'; @@ -182,9 +176,8 @@ function WalletPage() { if (isPersonalBankAccountMissingInfo(accountData) && accountData?.bankAccountID) { const additionalData = accountData?.additionalData; const [street1, street2] = additionalData?.addressStreet?.split('\n') ?? []; - clearPersonalBankAccount(); + resetPersonalBankAccountForUpdate(accountData.bankAccountID); clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - setPersonalBankAccountID(accountData.bankAccountID); Promise.all([ setDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, { legalFirstName: additionalData?.firstName, From 8e7484d33e756ce7781f2516bdc4decc8a62ef38 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 12:47:26 +0300 Subject: [PATCH 46/63] fix: use getStreetLines utility and add error handling in WalletPage --- src/pages/settings/Wallet/WalletPage/index.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 2cc6e6a9a947c..5173c038a84f4 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -37,9 +37,11 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import {hasDisplayableAssignedCards, isDirectFeed, maskCardNumber} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; +import Log from '@libs/Log'; import createDynamicRoute from '@libs/Navigation/helpers/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {formatPaymentMethods, getPaymentMethodDescription} from '@libs/PaymentUtils'; +import {getStreetLines} from '@libs/PersonalDetailsUtils'; import {getActiveAdminWorkspaces, getDescriptionForPolicyDomainCard, hasActiveAdminWorkspaces, hasEligibleActiveAdminFromWorkspaces, isPaidGroupPolicy} from '@libs/PolicyUtils'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; @@ -175,14 +177,14 @@ function WalletPage() { const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { if (isPersonalBankAccountMissingInfo(accountData) && accountData?.bankAccountID) { const additionalData = accountData?.additionalData; - const [street1, street2] = additionalData?.addressStreet?.split('\n') ?? []; + const [street1, street2] = additionalData?.addressStreet ? getStreetLines(additionalData.addressStreet) : []; resetPersonalBankAccountForUpdate(accountData.bankAccountID); clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); Promise.all([ setDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, { legalFirstName: additionalData?.firstName, legalLastName: additionalData?.lastName, - addressStreet: street1 || undefined, + addressStreet: street1, addressStreet2: street2 ?? '', addressCity: additionalData?.addressCity, addressState: additionalData?.addressState, @@ -190,16 +192,20 @@ function WalletPage() { phoneNumber: additionalData?.companyPhone, }), setDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM, { - addressLine1: street1 || undefined, + addressLine1: street1, addressLine2: street2 ?? '', city: additionalData?.addressCity, state: additionalData?.addressState, zipPostCode: additionalData?.addressZipCode, country: CONST.COUNTRY.US, }), - ]).then(() => { - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); - }); + ]) + .then(() => { + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); + }) + .catch((error: unknown) => { + Log.hmmm('[WalletPage] Failed to set draft values for personal bank account update', {error}); + }); return; } From c1bd227187ef6d661c88d8e67c35723d7b0ee5bf Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 12:59:47 +0300 Subject: [PATCH 47/63] refactor: make bankAccountID required, add displayName and logging, test hasPersonalBankAccountMissingInfo --- .../UpdatePersonalBankAccountInfoParams.ts | 2 +- .../Wallet/UpdatePersonalBankAccountPage.tsx | 6 +++ tests/unit/BankAccountUtilsTest.ts | 50 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts b/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts index 31ba99171c34a..c58f6acc85572 100644 --- a/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts +++ b/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts @@ -1,5 +1,5 @@ type UpdatePersonalBankAccountInfoParams = { - bankAccountID?: number; + bankAccountID: number; companyPhone?: string; legalFirstName?: string; legalLastName?: string; diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 39eae9fd50cfe..312d37259341a 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -12,6 +12,7 @@ import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCompletedStepsForBankAccount, PERSONAL_INFO_STEP} from '@libs/BankAccountUtils'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; +import Log from '@libs/Log'; import {getCurrentAddress, getStreetLines} from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import Navigation from '@navigation/Navigation'; @@ -45,6 +46,7 @@ function AddressWithDraft({isEditing, onNext, onMove}: SubStepProps) { /> ); } +AddressWithDraft.displayName = 'AddressWithDraft'; /** * Wrapper that delays auto-focus to avoid validation errors during URL-based navigation transitions. @@ -59,6 +61,7 @@ function DelayedPhoneNumber({isEditing, onNext, onMove}: SubStepProps) { /> ); } +DelayedPhoneNumber.displayName = 'DelayedPhoneNumber'; const formPages = [ {pageName: PAGE_NAME.LEGAL_NAME, component: LegalName}, @@ -141,6 +144,9 @@ function UpdatePersonalBankAccountPage() { updatePersonalBankAccountInfo(personalBankAccount.bankAccountID, accountData); }; const skipPageCandidates = getPageNamesForCompletedSteps(completedSteps); + if (skipPageCandidates.length >= formPages.length) { + Log.hmmm('[UpdatePersonalBankAccountPage] All steps already completed but user reached update flow', {bankAccountID}); + } const skipPages = skipPageCandidates.length >= formPages.length ? [] : skipPageCandidates; const firstPageName = getFirstPageName(bankAccountList, personalBankAccount?.bankAccountID); diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 557978bdee75d..46f4861ab03bf 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -3,6 +3,7 @@ import { getDefaultCompanyWebsite, getLastFourDigits, hasPartiallySetupBankAccount, + hasPersonalBankAccountMissingInfo, isBankAccountPartiallySetup, isPersonalBankAccountMissingInfo, PERSONAL_INFO_STEP, @@ -234,6 +235,55 @@ describe('BankAccountUtils', () => { }); }); + describe('hasPersonalBankAccountMissingInfo', () => { + it('returns true when at least one account has missing info', () => { + const bankAccountList = { + accountOne: { + accountData: { + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + additionalData: {country: CONST.COUNTRY.US, firstName: 'John'}, + }, + bankCurrency: 'USD', + bankCountry: 'US', + }, + } as unknown as BankAccountList; + expect(hasPersonalBankAccountMissingInfo(bankAccountList)).toBe(true); + }); + + it('returns false when all accounts have complete info', () => { + const bankAccountList = { + accountOne: { + accountData: { + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + additionalData: { + country: CONST.COUNTRY.US, + firstName: 'John', + lastName: 'Doe', + addressStreet: '123 Main St', + addressCity: 'New York', + addressState: 'NY', + addressZipCode: '10001', + companyPhone: '+15551234567', + }, + }, + bankCurrency: 'USD', + bankCountry: 'US', + }, + } as unknown as BankAccountList; + expect(hasPersonalBankAccountMissingInfo(bankAccountList)).toBe(false); + }); + + it('returns false for empty bank account list', () => { + expect(hasPersonalBankAccountMissingInfo({})).toBe(false); + }); + + it('returns false for undefined bank account list', () => { + expect(hasPersonalBankAccountMissingInfo(undefined)).toBe(false); + }); + }); + describe('getCompletedStepsForBankAccount', () => { const bankAccountID = 12345; const bankAccountKey = String(bankAccountID); From 03d642bc8f8a87e968d9642597123178d0f0bd6b Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 13:12:01 +0300 Subject: [PATCH 48/63] fix: atomic draft seeding, country validation with hidden selector --- src/components/AddressForm.tsx | 9 ++++--- src/libs/actions/BankAccounts.ts | 10 ++++---- .../Wallet/UpdatePersonalBankAccountPage.tsx | 13 ---------- .../settings/Wallet/WalletPage/index.tsx | 24 +++++++------------ 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 3218a7f227cee..6416b8e5e48ba 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -91,11 +91,14 @@ function AddressForm({ */ const validator = useCallback( - (values: FormOnyxValues): Errors => { + (rawValues: FormOnyxValues): Errors => { + // When hidden, the country input is unregistered so fall back to the country prop. + const values = shouldHideCountrySelector ? {...rawValues, country: rawValues.country || country} : rawValues; + const errors: Errors & { zipPostCode?: string | string[]; } = {}; - const requiredFields = ['addressLine1', 'city', 'country', 'state'] as const; + const requiredFields = shouldHideCountrySelector ? (['addressLine1', 'city', 'state'] as const) : (['addressLine1', 'city', 'country', 'state'] as const); // Check "State" dropdown is a valid state if selected Country is USA if (values.country === CONST.COUNTRY.US && !values.state) { @@ -149,7 +152,7 @@ function AddressForm({ return errors; }, - [translate], + [translate, shouldHideCountrySelector, country], ); return ( diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6a74b3265b521..225afc32cefc4 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -314,14 +314,12 @@ function setPersonalBankAccountID(bankAccountID: number) { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {bankAccountID}); } -/** - * Atomically resets personal bank account state and sets the bankAccountID using a single Onyx.set, - * avoiding the race condition between set(null) and merge({bankAccountID}) on the same key. - */ -function resetPersonalBankAccountForUpdate(bankAccountID: number) { +/** Atomically resets personal bank account state and seeds draft values using Onyx.set to avoid set/merge races. */ +function resetPersonalBankAccountForUpdate(bankAccountID: number, personalBankAccountDraft?: Partial, homeAddressDraft?: Record) { clearPlaid(); Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {bankAccountID}); - Onyx.set(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, null); + Onyx.set(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, personalBankAccountDraft ?? null); + Onyx.set(ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT, homeAddressDraft ?? null); } function clearOnfidoToken() { diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 312d37259341a..6c26360cbdf2f 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -31,10 +31,6 @@ const PAGE_NAME = CONST.UPDATE_PERSONAL_BANK_ACCOUNT.PAGE_NAME; const PAGE_NAMES: string[] = [PAGE_NAME.LEGAL_NAME, PAGE_NAME.ADDRESS, PAGE_NAME.PHONE_NUMBER]; -/** - * Wrapper that enables draft saving on the address form and hides the country selector - * since the update flow only supports US bank accounts. - */ function AddressWithDraft({isEditing, onNext, onMove}: SubStepProps) { return (
PAGE_NAMES.at(step - 1)).filter((name): name is string => !!name); } -/** - * Returns the first non-skipped page name for the update flow based on the bank account's existing data. - */ function getFirstPageName(bankAccountList?: OnyxEntry, bankAccountID?: number): string { const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; const skipPageNames = new Set(getPageNamesForCompletedSteps(completedSteps)); diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 5173c038a84f4..56ff4f5a0187c 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -37,7 +37,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import {hasDisplayableAssignedCards, isDirectFeed, maskCardNumber} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; -import Log from '@libs/Log'; import createDynamicRoute from '@libs/Navigation/helpers/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {formatPaymentMethods, getPaymentMethodDescription} from '@libs/PaymentUtils'; @@ -48,7 +47,6 @@ import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import {getFirstPageName} from '@pages/settings/Wallet/UpdatePersonalBankAccountPage'; import {deletePaymentBankAccount, openPersonalBankAccountSetupView, resetPersonalBankAccountForUpdate, setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; -import {clearDraftValues, setDraftValues} from '@userActions/FormActions'; import {close as closeModal} from '@userActions/Modal'; import {clearWalletError, clearWalletTermsError, deletePaymentCard, getPaymentMethods, makeDefaultPaymentMethod as makeDefaultPaymentMethodPaymentMethods} from '@userActions/PaymentMethods'; import {enableCompanyCards} from '@userActions/Policy/Policy'; @@ -178,10 +176,9 @@ function WalletPage() { if (isPersonalBankAccountMissingInfo(accountData) && accountData?.bankAccountID) { const additionalData = accountData?.additionalData; const [street1, street2] = additionalData?.addressStreet ? getStreetLines(additionalData.addressStreet) : []; - resetPersonalBankAccountForUpdate(accountData.bankAccountID); - clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - Promise.all([ - setDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, { + resetPersonalBankAccountForUpdate( + accountData.bankAccountID, + { legalFirstName: additionalData?.firstName, legalLastName: additionalData?.lastName, addressStreet: street1, @@ -190,22 +187,17 @@ function WalletPage() { addressState: additionalData?.addressState, addressZipCode: additionalData?.addressZipCode, phoneNumber: additionalData?.companyPhone, - }), - setDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM, { + }, + { addressLine1: street1, addressLine2: street2 ?? '', city: additionalData?.addressCity, state: additionalData?.addressState, zipPostCode: additionalData?.addressZipCode, country: CONST.COUNTRY.US, - }), - ]) - .then(() => { - Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); - }) - .catch((error: unknown) => { - Log.hmmm('[WalletPage] Failed to set draft values for personal bank account update', {error}); - }); + }, + ); + Navigation.navigate(ROUTES.SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT.getRoute(getFirstPageName(bankAccountList, accountData.bankAccountID))); return; } From 7368821cd1b4c8b3037e606bc191c0d74e0bd3ed Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 13:34:27 +0300 Subject: [PATCH 49/63] fix: use null instead of undefined in rollback to ensure Onyx merge clears optimistic values --- src/libs/actions/BankAccounts.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 225afc32cefc4..87e4f301a8840 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -169,6 +169,8 @@ function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Parti ...(accountData?.phoneNumber && {companyPhone: accountData.phoneNumber}), }; + type AdditionalDataRollback = {[K in keyof AdditionalDataUpdate]: AdditionalDataUpdate[K] | null}; + const bankAccountListUpdates: Record = { [bankAccountKey]: { accountData: { @@ -177,17 +179,17 @@ function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Parti }, }; - const bankAccountListRollback: Record = { + const bankAccountListRollback: Record = { [bankAccountKey]: { accountData: { additionalData: { - firstName: prevData?.firstName, - lastName: prevData?.lastName, - addressStreet: prevData?.addressStreet, - addressCity: prevData?.addressCity, - addressState: prevData?.addressState, - addressZipCode: prevData?.addressZipCode, - companyPhone: prevData?.companyPhone, + firstName: prevData?.firstName ?? null, + lastName: prevData?.lastName ?? null, + addressStreet: prevData?.addressStreet ?? null, + addressCity: prevData?.addressCity ?? null, + addressState: prevData?.addressState ?? null, + addressZipCode: prevData?.addressZipCode ?? null, + companyPhone: prevData?.companyPhone ?? null, }, }, }, From 50d274ae26d70c2b7f3243d2c2c5b34b3389ead5 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 13:49:28 +0300 Subject: [PATCH 50/63] fix: Remove redundant useMemo/useCallback (React Compiler handles memoization) --- src/components/AddressForm.tsx | 110 ++++++++---------- .../settings/Wallet/PaymentMethodListItem.tsx | 34 +++--- 2 files changed, 65 insertions(+), 79 deletions(-) diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 6416b8e5e48ba..736d1af916e64 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -83,77 +83,67 @@ function AddressForm({ const isUSAForm = country === CONST.COUNTRY.US; - /** - * @param translate - translate function - * @param isUSAForm - selected country ISO code is US - * @param values - form input values - * @returns - An object containing the errors for each inputID - */ - - const validator = useCallback( - (rawValues: FormOnyxValues): Errors => { - // When hidden, the country input is unregistered so fall back to the country prop. - const values = shouldHideCountrySelector ? {...rawValues, country: rawValues.country || country} : rawValues; - - const errors: Errors & { - zipPostCode?: string | string[]; - } = {}; - const requiredFields = shouldHideCountrySelector ? (['addressLine1', 'city', 'state'] as const) : (['addressLine1', 'city', 'country', 'state'] as const); - - // Check "State" dropdown is a valid state if selected Country is USA - if (values.country === CONST.COUNTRY.US && !values.state) { - errors.state = translate('common.error.fieldRequired'); + const validator = (rawValues: FormOnyxValues): Errors => { + // When hidden, the country input is unregistered so fall back to the country prop. + const values = shouldHideCountrySelector ? {...rawValues, country: rawValues.country || country} : rawValues; + + const errors: Errors & { + zipPostCode?: string | string[]; + } = {}; + const requiredFields = shouldHideCountrySelector ? (['addressLine1', 'city', 'state'] as const) : (['addressLine1', 'city', 'country', 'state'] as const); + + // Check "State" dropdown is a valid state if selected Country is USA + if (values.country === CONST.COUNTRY.US && !values.state) { + errors.state = translate('common.error.fieldRequired'); + } + + // Add "Field required" errors if any required field is empty + for (const fieldKey of requiredFields) { + const fieldValue = values[fieldKey] ?? ''; + if (isRequiredFulfilled(fieldValue)) { + continue; } - // Add "Field required" errors if any required field is empty - for (const fieldKey of requiredFields) { - const fieldValue = values[fieldKey] ?? ''; - if (isRequiredFulfilled(fieldValue)) { - continue; - } - - errors[fieldKey] = translate('common.error.fieldRequired'); - } + errors[fieldKey] = translate('common.error.fieldRequired'); + } - if (values.addressLine1.length > CONST.FORM_CHARACTER_LIMIT) { - errors.addressLine1 = translate('common.error.characterLimitExceedCounter', values.addressLine1.length, CONST.FORM_CHARACTER_LIMIT); - } + if (values.addressLine1.length > CONST.FORM_CHARACTER_LIMIT) { + errors.addressLine1 = translate('common.error.characterLimitExceedCounter', values.addressLine1.length, CONST.FORM_CHARACTER_LIMIT); + } - if (values.addressLine2.length > CONST.FORM_CHARACTER_LIMIT) { - errors.addressLine2 = translate('common.error.characterLimitExceedCounter', values.addressLine2.length, CONST.FORM_CHARACTER_LIMIT); - } + if (values.addressLine2.length > CONST.FORM_CHARACTER_LIMIT) { + errors.addressLine2 = translate('common.error.characterLimitExceedCounter', values.addressLine2.length, CONST.FORM_CHARACTER_LIMIT); + } - if (values.city.length > CONST.FORM_CHARACTER_LIMIT) { - errors.city = translate('common.error.characterLimitExceedCounter', values.city.length, CONST.FORM_CHARACTER_LIMIT); - } + if (values.city.length > CONST.FORM_CHARACTER_LIMIT) { + errors.city = translate('common.error.characterLimitExceedCounter', values.city.length, CONST.FORM_CHARACTER_LIMIT); + } - if (values.country !== CONST.COUNTRY.US && values.state.length > CONST.STATE_CHARACTER_LIMIT) { - errors.state = translate('common.error.characterLimitExceedCounter', values.state.length, CONST.STATE_CHARACTER_LIMIT); - } + if (values.country !== CONST.COUNTRY.US && values.state.length > CONST.STATE_CHARACTER_LIMIT) { + errors.state = translate('common.error.characterLimitExceedCounter', values.state.length, CONST.STATE_CHARACTER_LIMIT); + } - // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object - const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; + // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object + const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; - // The postal code system might not exist for a country, so no regex either for them. - const countrySpecificZipRegex = countryRegexDetails?.regex; - const countryZipFormat = countryRegexDetails?.samples ?? ''; + // The postal code system might not exist for a country, so no regex either for them. + const countrySpecificZipRegex = countryRegexDetails?.regex; + const countryZipFormat = countryRegexDetails?.samples ?? ''; - if (countrySpecificZipRegex) { - if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) { - if (isRequiredFulfilled(values.zipPostCode?.trim())) { - errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat); - } else { - errors.zipPostCode = translate('common.error.fieldRequired'); - } + if (countrySpecificZipRegex) { + if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) { + if (isRequiredFulfilled(values.zipPostCode?.trim())) { + errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat); + } else { + errors.zipPostCode = translate('common.error.fieldRequired'); } - } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { - errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat'); } + } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { + errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat'); + } - return errors; - }, - [translate, shouldHideCountrySelector, country], - ); + return errors; + }; return ( { - if (isNeedingAction) { - return translate('common.actionRequired'); - } - if (item.isCardFrozen) { - return translate('cardPage.frozen'); - } - return shouldShowDefaultBadge ? translate('paymentMethodList.defaultPaymentMethod') : undefined; - }, [isNeedingAction, item.isCardFrozen, shouldShowDefaultBadge, translate]); + let badgeText; + if (isNeedingAction) { + badgeText = translate('common.actionRequired'); + } else if (item.isCardFrozen) { + badgeText = translate('cardPage.frozen'); + } else if (shouldShowDefaultBadge) { + badgeText = translate('paymentMethodList.defaultPaymentMethod'); + } - const badgeIcon = useMemo(() => { - if (isNeedingAction) { - return icons.DotIndicator; - } - if (item.isCardFrozen) { - return icons.FreezeCard; - } - return undefined; - }, [icons.DotIndicator, icons.FreezeCard, isNeedingAction, item.isCardFrozen]); + let badgeIcon; + if (isNeedingAction) { + badgeIcon = icons.DotIndicator; + } else if (item.isCardFrozen) { + badgeIcon = icons.FreezeCard; + } return ( Date: Tue, 10 Mar 2026 13:58:42 +0300 Subject: [PATCH 51/63] chore: Remove dead setPersonalBankAccountID --- src/libs/actions/BankAccounts.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 87e4f301a8840..4d64d22d487e5 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -312,10 +312,6 @@ function clearPersonalBankAccount() { clearPersonalBankAccountSetupType(); } -function setPersonalBankAccountID(bankAccountID: number) { - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {bankAccountID}); -} - /** Atomically resets personal bank account state and seeds draft values using Onyx.set to avoid set/merge races. */ function resetPersonalBankAccountForUpdate(bankAccountID: number, personalBankAccountDraft?: Partial, homeAddressDraft?: Record) { clearPlaid(); @@ -1661,7 +1657,6 @@ export { clearOnfidoToken, clearPersonalBankAccount, resetPersonalBankAccountForUpdate, - setPersonalBankAccountID, setPlaidEvent, openPlaidView, connectBankAccountManually, From d0c38964b5849172eb6accdd44fa67014a81a207 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 14:04:27 +0300 Subject: [PATCH 52/63] chore: Restore useCallback in AddressForm validator --- src/components/AddressForm.tsx | 103 +++++++++++++++++---------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 736d1af916e64..16cc6e840db54 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -83,67 +83,70 @@ function AddressForm({ const isUSAForm = country === CONST.COUNTRY.US; - const validator = (rawValues: FormOnyxValues): Errors => { - // When hidden, the country input is unregistered so fall back to the country prop. - const values = shouldHideCountrySelector ? {...rawValues, country: rawValues.country || country} : rawValues; - - const errors: Errors & { - zipPostCode?: string | string[]; - } = {}; - const requiredFields = shouldHideCountrySelector ? (['addressLine1', 'city', 'state'] as const) : (['addressLine1', 'city', 'country', 'state'] as const); - - // Check "State" dropdown is a valid state if selected Country is USA - if (values.country === CONST.COUNTRY.US && !values.state) { - errors.state = translate('common.error.fieldRequired'); - } - - // Add "Field required" errors if any required field is empty - for (const fieldKey of requiredFields) { - const fieldValue = values[fieldKey] ?? ''; - if (isRequiredFulfilled(fieldValue)) { - continue; + const validator = useCallback( + (rawValues: FormOnyxValues): Errors => { + // When hidden, the country input is unregistered so fall back to the country prop. + const values = shouldHideCountrySelector ? {...rawValues, country: rawValues.country || country} : rawValues; + + const errors: Errors & { + zipPostCode?: string | string[]; + } = {}; + const requiredFields = shouldHideCountrySelector ? (['addressLine1', 'city', 'state'] as const) : (['addressLine1', 'city', 'country', 'state'] as const); + + // Check "State" dropdown is a valid state if selected Country is USA + if (values.country === CONST.COUNTRY.US && !values.state) { + errors.state = translate('common.error.fieldRequired'); } - errors[fieldKey] = translate('common.error.fieldRequired'); - } + // Add "Field required" errors if any required field is empty + for (const fieldKey of requiredFields) { + const fieldValue = values[fieldKey] ?? ''; + if (isRequiredFulfilled(fieldValue)) { + continue; + } + + errors[fieldKey] = translate('common.error.fieldRequired'); + } - if (values.addressLine1.length > CONST.FORM_CHARACTER_LIMIT) { - errors.addressLine1 = translate('common.error.characterLimitExceedCounter', values.addressLine1.length, CONST.FORM_CHARACTER_LIMIT); - } + if (values.addressLine1.length > CONST.FORM_CHARACTER_LIMIT) { + errors.addressLine1 = translate('common.error.characterLimitExceedCounter', values.addressLine1.length, CONST.FORM_CHARACTER_LIMIT); + } - if (values.addressLine2.length > CONST.FORM_CHARACTER_LIMIT) { - errors.addressLine2 = translate('common.error.characterLimitExceedCounter', values.addressLine2.length, CONST.FORM_CHARACTER_LIMIT); - } + if (values.addressLine2.length > CONST.FORM_CHARACTER_LIMIT) { + errors.addressLine2 = translate('common.error.characterLimitExceedCounter', values.addressLine2.length, CONST.FORM_CHARACTER_LIMIT); + } - if (values.city.length > CONST.FORM_CHARACTER_LIMIT) { - errors.city = translate('common.error.characterLimitExceedCounter', values.city.length, CONST.FORM_CHARACTER_LIMIT); - } + if (values.city.length > CONST.FORM_CHARACTER_LIMIT) { + errors.city = translate('common.error.characterLimitExceedCounter', values.city.length, CONST.FORM_CHARACTER_LIMIT); + } - if (values.country !== CONST.COUNTRY.US && values.state.length > CONST.STATE_CHARACTER_LIMIT) { - errors.state = translate('common.error.characterLimitExceedCounter', values.state.length, CONST.STATE_CHARACTER_LIMIT); - } + if (values.country !== CONST.COUNTRY.US && values.state.length > CONST.STATE_CHARACTER_LIMIT) { + errors.state = translate('common.error.characterLimitExceedCounter', values.state.length, CONST.STATE_CHARACTER_LIMIT); + } - // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object - const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; + // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object + const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; - // The postal code system might not exist for a country, so no regex either for them. - const countrySpecificZipRegex = countryRegexDetails?.regex; - const countryZipFormat = countryRegexDetails?.samples ?? ''; + // The postal code system might not exist for a country, so no regex either for them. + const countrySpecificZipRegex = countryRegexDetails?.regex; + const countryZipFormat = countryRegexDetails?.samples ?? ''; - if (countrySpecificZipRegex) { - if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) { - if (isRequiredFulfilled(values.zipPostCode?.trim())) { - errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat); - } else { - errors.zipPostCode = translate('common.error.fieldRequired'); + if (countrySpecificZipRegex) { + if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) { + if (isRequiredFulfilled(values.zipPostCode?.trim())) { + errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat); + } else { + errors.zipPostCode = translate('common.error.fieldRequired'); + } } + } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { + errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat'); } - } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { - errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat'); - } - return errors; - }; + return errors; + }, + [translate, shouldHideCountrySelector, country], + ); return ( Date: Tue, 10 Mar 2026 14:39:36 +0300 Subject: [PATCH 53/63] chore: Restore removed comment during merge --- src/components/AddressForm.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 16cc6e840db54..6416b8e5e48ba 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -83,6 +83,13 @@ function AddressForm({ const isUSAForm = country === CONST.COUNTRY.US; + /** + * @param translate - translate function + * @param isUSAForm - selected country ISO code is US + * @param values - form input values + * @returns - An object containing the errors for each inputID + */ + const validator = useCallback( (rawValues: FormOnyxValues): Errors => { // When hidden, the country input is unregistered so fall back to the country prop. From 1d70b52947ef152aac30099fa2f43b8002897f20 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 10 Mar 2026 20:22:02 +0300 Subject: [PATCH 54/63] fix: draft wipe on back navigation between substeps --- .../Wallet/UpdatePersonalBankAccountPage.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 6c26360cbdf2f..06cfbb0e226d4 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -89,15 +89,10 @@ function UpdatePersonalBankAccountPage() { const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; - useEffect(() => { - return () => { - clearPersonalBankAccount(); - clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); - clearDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); - }; - }, []); - const exitFlow = () => { + clearPersonalBankAccount(); + clearDraftValues(ONYXKEYS.FORMS.HOME_ADDRESS_FORM); + clearDraftValues(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); Navigation.goBack(ROUTES.SETTINGS_WALLET); }; From 61950578e6dd495735c99d17c3f5e4be50200207 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 13 Mar 2026 02:17:17 +0300 Subject: [PATCH 55/63] fix: require all API params, remove form entries from onyxData, remove unused edit action from route --- src/ROUTES.ts | 6 +- .../UpdatePersonalBankAccountInfoParams.ts | 14 +- src/libs/Navigation/types.ts | 1 - src/libs/actions/BankAccounts.ts | 131 ++++++------------ .../Wallet/UpdatePersonalBankAccountPage.tsx | 52 +++---- 5 files changed, 78 insertions(+), 126 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0b9c360996445..fefdeaed7daaa 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -464,12 +464,12 @@ const ROUTES = { SETTINGS_ADD_US_BANK_ACCOUNT: 'settings/wallet/add-us-bank-account', SETTINGS_ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'settings/wallet/add-us-bank-account/entry-point', SETTINGS_UPDATE_PERSONAL_BANK_ACCOUNT: { - route: 'settings/wallet/update-personal-bank-account/:subPage?/:action?', - getRoute: (subPage?: string, action?: 'edit') => { + route: 'settings/wallet/update-personal-bank-account/:subPage?', + getRoute: (subPage?: string) => { if (!subPage) { return 'settings/wallet/update-personal-bank-account' as const; } - return `settings/wallet/update-personal-bank-account/${subPage}${action ? `/${action}` : ''}` as const; + return `settings/wallet/update-personal-bank-account/${subPage}` as const; }, }, SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: `settings/wallet/add-bank-account/select-country/${VERIFY_ACCOUNT}`, diff --git a/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts b/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts index c58f6acc85572..79f5aa4b3a97f 100644 --- a/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts +++ b/src/libs/API/parameters/UpdatePersonalBankAccountInfoParams.ts @@ -1,12 +1,12 @@ type UpdatePersonalBankAccountInfoParams = { bankAccountID: number; - companyPhone?: string; - legalFirstName?: string; - legalLastName?: string; - addressStreet?: string; - addressCity?: string; - addressState?: string; - addressZip?: string; + companyPhone: string; + legalFirstName: string; + legalLastName: string; + addressStreet: string; + addressCity: string; + addressState: string; + addressZip: string; }; export default UpdatePersonalBankAccountInfoParams; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 10af9de56344d..5769a74f04b8e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -228,7 +228,6 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT_ENTRY_POINT]: undefined; [SCREENS.SETTINGS.UPDATE_PERSONAL_BANK_ACCOUNT]: { subPage?: string; - action?: 'edit'; }; [SCREENS.SETTINGS.BANK_ACCOUNT_PURPOSE]: undefined; [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: undefined; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 86e469e4464a8..a73d86d286ec5 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -151,69 +151,36 @@ function clearPersonalBankAccountErrors() { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); } -function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Partial) { - const formattedStreet = getFormattedStreet(accountData?.addressStreet, accountData?.addressStreet2); - - type AdditionalDataUpdate = Partial>; +function updatePersonalBankAccountInfo(bankAccountID: number, accountData: PersonalBankAccountForm) { + const formattedStreet = getFormattedStreet(accountData.addressStreet, accountData.addressStreet2); const bankAccountKey = String(bankAccountID); const prevData = bankAccountList?.[bankAccountKey]?.accountData?.additionalData; - const additionalDataUpdate: AdditionalDataUpdate = { - ...(accountData?.legalFirstName && {firstName: accountData.legalFirstName}), - ...(accountData?.legalLastName && {lastName: accountData.legalLastName}), - ...(formattedStreet && {addressStreet: formattedStreet}), - ...(accountData?.addressCity && {addressCity: accountData.addressCity}), - ...(accountData?.addressState && {addressState: accountData.addressState}), - ...(accountData?.addressZipCode && {addressZipCode: accountData.addressZipCode}), - ...(accountData?.phoneNumber && {companyPhone: accountData.phoneNumber}), - }; - - type AdditionalDataRollback = {[K in keyof AdditionalDataUpdate]: AdditionalDataUpdate[K] | null}; + type AdditionalDataFields = Pick; - const bankAccountListUpdates: Record = { - [bankAccountKey]: { - accountData: { - additionalData: additionalDataUpdate, - }, - }, - }; - - const bankAccountListRollback: Record = { - [bankAccountKey]: { - accountData: { - additionalData: { - firstName: prevData?.firstName ?? null, - lastName: prevData?.lastName ?? null, - addressStreet: prevData?.addressStreet ?? null, - addressCity: prevData?.addressCity ?? null, - addressState: prevData?.addressState ?? null, - addressZipCode: prevData?.addressZipCode ?? null, - companyPhone: prevData?.companyPhone ?? null, - }, - }, - }, + const additionalDataUpdate: AdditionalDataFields = { + firstName: accountData.legalFirstName, + lastName: accountData.legalLastName, + addressStreet: formattedStreet, + addressCity: accountData.addressCity, + addressState: accountData.addressState, + addressZipCode: accountData.addressZipCode, + companyPhone: accountData.phoneNumber, }; const parameters: UpdatePersonalBankAccountInfoParams = { bankAccountID, - ...(accountData?.phoneNumber && {companyPhone: accountData.phoneNumber}), - ...(accountData?.legalFirstName && {legalFirstName: accountData.legalFirstName}), - ...(accountData?.legalLastName && {legalLastName: accountData.legalLastName}), - ...(formattedStreet && {addressStreet: formattedStreet}), - ...(accountData?.addressCity && {addressCity: accountData.addressCity}), - ...(accountData?.addressState && {addressState: accountData.addressState}), - ...(accountData?.addressZipCode && {addressZip: accountData.addressZipCode}), + companyPhone: accountData.phoneNumber, + legalFirstName: accountData.legalFirstName, + legalLastName: accountData.legalLastName, + addressStreet: formattedStreet, + addressCity: accountData.addressCity, + addressState: accountData.addressState, + addressZip: accountData.addressZipCode, }; - const onyxData: OnyxData< - | typeof ONYXKEYS.PERSONAL_BANK_ACCOUNT - | typeof ONYXKEYS.BANK_ACCOUNT_LIST - | typeof ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM - | typeof ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT - | typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM - | typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT - > = { + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -225,23 +192,15 @@ function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Parti }, { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, - value: { - errors: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.HOME_ADDRESS_FORM, + key: ONYXKEYS.BANK_ACCOUNT_LIST, value: { - errors: null, + [bankAccountKey]: { + accountData: { + additionalData: additionalDataUpdate, + }, + }, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.BANK_ACCOUNT_LIST, - value: bankAccountListUpdates, - }, ], successData: [ { @@ -253,30 +212,6 @@ function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Parti shouldShowSuccess: true, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, - value: { - errors: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.HOME_ADDRESS_FORM, - value: { - errors: null, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT, - value: null, - }, ], failureData: [ { @@ -290,7 +225,21 @@ function updatePersonalBankAccountInfo(bankAccountID: number, accountData: Parti { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.BANK_ACCOUNT_LIST, - value: bankAccountListRollback, + value: { + [bankAccountKey]: { + accountData: { + additionalData: { + firstName: prevData?.firstName ?? null, + lastName: prevData?.lastName ?? null, + addressStreet: prevData?.addressStreet ?? null, + addressCity: prevData?.addressCity ?? null, + addressState: prevData?.addressState ?? null, + addressZipCode: prevData?.addressZipCode ?? null, + companyPhone: prevData?.companyPhone ?? null, + }, + }, + }, + }, }, ], }; diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 06cfbb0e226d4..f08d17164fe53 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -10,7 +10,7 @@ import useOnyx from '@hooks/useOnyx'; import useSubPage from '@hooks/useSubPage'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCompletedStepsForBankAccount, PERSONAL_INFO_STEP} from '@libs/BankAccountUtils'; +import {getCompletedStepsForBankAccount} from '@libs/BankAccountUtils'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Log from '@libs/Log'; import {getCurrentAddress, getStreetLines} from '@libs/PersonalDetailsUtils'; @@ -101,29 +101,33 @@ function UpdatePersonalBankAccountPage() { return; } - const accountData: Partial = {}; - - if (!completedSteps.includes(PERSONAL_INFO_STEP.NAME)) { - accountData.legalFirstName = personalBankAccountDraft?.legalFirstName ?? privatePersonalDetails?.legalFirstName; - accountData.legalLastName = personalBankAccountDraft?.legalLastName ?? privatePersonalDetails?.legalLastName; - } - if (!completedSteps.includes(PERSONAL_INFO_STEP.ADDRESS)) { - const currentAddress = getCurrentAddress(privatePersonalDetails); - const [street1, street2] = getStreetLines(currentAddress?.street); - accountData.addressStreet = (personalBankAccountDraft?.addressStreet ? personalBankAccountDraft.addressStreet : undefined) ?? homeAddressDraft?.addressLine1 ?? street1; - accountData.addressStreet2 = (personalBankAccountDraft?.addressStreet2 ? personalBankAccountDraft.addressStreet2 : undefined) ?? homeAddressDraft?.addressLine2 ?? street2; - accountData.addressCity = (personalBankAccountDraft?.addressCity ? personalBankAccountDraft.addressCity : undefined) ?? homeAddressDraft?.city ?? currentAddress?.city; - accountData.addressState = (personalBankAccountDraft?.addressState ? personalBankAccountDraft.addressState : undefined) ?? homeAddressDraft?.state ?? currentAddress?.state; - accountData.addressZipCode = - (personalBankAccountDraft?.addressZipCode ? personalBankAccountDraft.addressZipCode : undefined) ?? homeAddressDraft?.zipPostCode ?? currentAddress?.zip; - } - if (!completedSteps.includes(PERSONAL_INFO_STEP.PHONE)) { - const finalPhoneNumber = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? ''; - const parsed = parsePhoneNumber(finalPhoneNumber, {regionCode: CONST.COUNTRY.US}); - accountData.phoneNumber = parsed.number?.significant ?? ''; - } - - updatePersonalBankAccountInfo(personalBankAccount.bankAccountID, accountData); + const existingData = bankAccountList?.[String(personalBankAccount.bankAccountID)]?.accountData?.additionalData; + const currentAddress = getCurrentAddress(privatePersonalDetails); + const [street1, street2] = getStreetLines(currentAddress?.street); + + const legalFirstName = personalBankAccountDraft?.legalFirstName ?? privatePersonalDetails?.legalFirstName ?? existingData?.firstName ?? ''; + const legalLastName = personalBankAccountDraft?.legalLastName ?? privatePersonalDetails?.legalLastName ?? existingData?.lastName ?? ''; + + const addressStreet = personalBankAccountDraft?.addressStreet ?? homeAddressDraft?.addressLine1 ?? street1 ?? existingData?.addressStreet ?? ''; + const addressStreet2 = personalBankAccountDraft?.addressStreet2 ?? homeAddressDraft?.addressLine2 ?? street2 ?? ''; + const addressCity = personalBankAccountDraft?.addressCity ?? homeAddressDraft?.city ?? currentAddress?.city ?? existingData?.addressCity ?? ''; + const addressState = personalBankAccountDraft?.addressState ?? homeAddressDraft?.state ?? currentAddress?.state ?? existingData?.addressState ?? ''; + const addressZipCode = personalBankAccountDraft?.addressZipCode ?? homeAddressDraft?.zipPostCode ?? currentAddress?.zip ?? existingData?.addressZipCode ?? ''; + + const rawPhone = personalBankAccountDraft?.phoneNumber ?? privatePersonalDetails?.phoneNumber ?? existingData?.companyPhone ?? ''; + const parsed = parsePhoneNumber(rawPhone, {regionCode: CONST.COUNTRY.US}); + const phoneNumber = parsed.number?.significant ?? ''; + + updatePersonalBankAccountInfo(personalBankAccount.bankAccountID, { + legalFirstName, + legalLastName, + addressStreet, + addressStreet2, + addressCity, + addressState, + addressZipCode, + phoneNumber, + } as PersonalBankAccountForm); }; const skipPageCandidates = getPageNamesForCompletedSteps(completedSteps); if (skipPageCandidates.length >= formPages.length) { From e6bb9694c12ae3408075cc3a56d78aea62dbbf9a Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 13 Mar 2026 02:21:12 +0300 Subject: [PATCH 56/63] fix: Replace PersonalBankAccountForm cast with typed PersonalBankAccountUpdateData Pick --- src/libs/actions/BankAccounts.ts | 7 ++++++- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index a73d86d286ec5..ff4f2df4916c6 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -151,7 +151,12 @@ function clearPersonalBankAccountErrors() { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); } -function updatePersonalBankAccountInfo(bankAccountID: number, accountData: PersonalBankAccountForm) { +type PersonalBankAccountUpdateData = Pick< + PersonalBankAccountForm, + 'legalFirstName' | 'legalLastName' | 'addressStreet' | 'addressStreet2' | 'addressCity' | 'addressState' | 'addressZipCode' | 'phoneNumber' +>; + +function updatePersonalBankAccountInfo(bankAccountID: number, accountData: PersonalBankAccountUpdateData) { const formattedStreet = getFormattedStreet(accountData.addressStreet, accountData.addressStreet2); const bankAccountKey = String(bankAccountID); diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index f08d17164fe53..7d37d4f5c198c 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -21,7 +21,6 @@ import {clearDraftValues} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalBankAccountForm} from '@src/types/form/PersonalBankAccountForm'; import type {BankAccountList} from '@src/types/onyx'; import Address from './InternationalDepositAccount/PersonalInfo/substeps/AddressStep'; import LegalName from './InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep'; @@ -127,7 +126,7 @@ function UpdatePersonalBankAccountPage() { addressState, addressZipCode, phoneNumber, - } as PersonalBankAccountForm); + }); }; const skipPageCandidates = getPageNamesForCompletedSteps(completedSteps); if (skipPageCandidates.length >= formPages.length) { From d33deafbeabcb09d8d5783be1c62003aac1396e4 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 13 Mar 2026 22:00:01 +0300 Subject: [PATCH 57/63] fix: Revert unnecessary change to getFormattedStreet shared utility --- src/libs/PersonalDetailsUtils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index f66d5bbfbf15d..11bfaf777afeb 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -274,9 +274,6 @@ function formatPiece(piece?: string): string { * @returns formatted street */ function getFormattedStreet(street1 = '', street2 = '') { - if (!street2) { - return street1; - } return `${street1}\n${street2}`; } From 686b8c034c2da186487df8e4af8c6c85db504ac6 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 13 Mar 2026 22:12:16 +0300 Subject: [PATCH 58/63] fix: Remove duplicate error message on API failure --- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 7d37d4f5c198c..55d909c042328 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,7 +1,6 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; -import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -11,7 +10,6 @@ import useSubPage from '@hooks/useSubPage'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCompletedStepsForBankAccount} from '@libs/BankAccountUtils'; -import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Log from '@libs/Log'; import {getCurrentAddress, getStreetLines} from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; @@ -84,7 +82,6 @@ function UpdatePersonalBankAccountPage() { const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const bankAccountID = personalBankAccount?.bankAccountID; - const errorMessage = getLatestErrorMessage(personalBankAccount ?? {}); const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; @@ -200,13 +197,6 @@ function UpdatePersonalBankAccountPage() { onNext={handleNextPage} onMove={() => {}} /> - {!!errorMessage && ( - - )} ); } From d26ce786e3d63c3e76f0ab077acbaad57566d362 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 16 Mar 2026 12:18:54 +0300 Subject: [PATCH 59/63] fix: Add unmount cleanup and restore error display on API failure --- .../Wallet/UpdatePersonalBankAccountPage.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 55d909c042328..e7e8578199b70 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; +import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -10,6 +11,7 @@ import useSubPage from '@hooks/useSubPage'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCompletedStepsForBankAccount} from '@libs/BankAccountUtils'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Log from '@libs/Log'; import {getCurrentAddress, getStreetLines} from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; @@ -82,6 +84,9 @@ function UpdatePersonalBankAccountPage() { const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const bankAccountID = personalBankAccount?.bankAccountID; + const errorMessage = getLatestErrorMessage(personalBankAccount ?? {}); + + useEffect(() => clearPersonalBankAccount, []); const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; @@ -197,6 +202,13 @@ function UpdatePersonalBankAccountPage() { onNext={handleNextPage} onMove={() => {}} /> + {!!errorMessage && ( + + )} ); } From 37266453e6852a77e409d15e3582d5499a97b647 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Wed, 18 Mar 2026 22:57:19 +0300 Subject: [PATCH 60/63] fix: Disable Next button when offline in update flow, add unmount cleanup and restore error display --- src/components/AddressForm.tsx | 6 +++++- src/components/SubStepForms/FullNameStep.tsx | 6 +++++- src/libs/BankAccountUtils.ts | 10 ++++++++++ .../PersonalInfo/substeps/AddressStep.tsx | 6 +++++- .../PersonalInfo/substeps/LegalNameStep.tsx | 8 +++++++- .../PersonalInfo/substeps/PhoneNumberStep.tsx | 7 +++++-- .../Wallet/UpdatePersonalBankAccountPage.tsx | 16 +++++++++++++++- 7 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 19db54ff8b16d..e354ecc1c4bae 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -58,6 +58,9 @@ type AddressFormProps = { /** Whether to hide the country selector (e.g. when country cannot be changed) */ shouldHideCountrySelector?: boolean; + + /** Whether the form submit button should be enabled when offline */ + enabledWhenOffline?: boolean; }; function AddressForm({ @@ -73,6 +76,7 @@ function AddressForm({ submitButtonText = '', zip = '', shouldHideCountrySelector = false, + enabledWhenOffline: enabledWhenOfflineProp = true, }: AddressFormProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -162,7 +166,7 @@ function AddressForm({ validate={validator} onSubmit={onSubmit} submitButtonText={submitButtonText} - enabledWhenOffline + enabledWhenOffline={enabledWhenOfflineProp} addBottomSafeAreaPadding > diff --git a/src/components/SubStepForms/FullNameStep.tsx b/src/components/SubStepForms/FullNameStep.tsx index 92660b1b78e41..d536805e6e5ab 100644 --- a/src/components/SubStepForms/FullNameStep.tsx +++ b/src/components/SubStepForms/FullNameStep.tsx @@ -55,6 +55,9 @@ type FullNameStepProps = SubStepPro /** Whether to show the Patriot Act help link (EnablePayments-only) */ shouldShowPatriotActLink?: boolean; + + /** Whether the form submit button should be enabled when offline */ + enabledWhenOffline?: boolean; }; function FullNameStep({ @@ -72,6 +75,7 @@ function FullNameStep({ customLastNameLabel, shouldShowPatriotActLink = false, forwardedFSClass, + enabledWhenOffline: enabledWhenOfflineProp = true, }: FullNameStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -125,7 +129,7 @@ function FullNameStep({ validate={customValidate ?? validate} onSubmit={onSubmit} style={[styles.mh5, styles.flexGrow1]} - enabledWhenOffline + enabledWhenOffline={enabledWhenOfflineProp} > {formTitle} diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index e73a0aa09e364..d68fadea2009c 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -58,6 +58,11 @@ function hasOwnerPhone(additionalData: AdditionalData): boolean { * Check if a US personal bank account in OPEN state is missing required personal information. */ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): boolean { + // TODO: REMOVE MOCK - force missing info for testing + if (accountData?.bankAccountID === 8952303) { + return true; + } + if (accountData?.type !== CONST.BANK_ACCOUNT.TYPE.PERSONAL) { return false; } @@ -81,6 +86,11 @@ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): * Returns step numbers that already have data on the bank account and can be skipped in the update flow. */ function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry, bankAccountID: number): number[] { + // TODO: REMOVE MOCK - force all steps to show for testing + if (bankAccountID === 8952303) { + return []; + } + const bankAccount = bankAccountList?.[String(bankAccountID)]; if (!bankAccount) { return []; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx index 7cc2ff61c4a11..ecba9775df7be 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx @@ -22,9 +22,12 @@ type AddressStepProps = SubStepProps & { /** Whether to hide the country selector (e.g. when country cannot be changed) */ shouldHideCountrySelector?: boolean; + + /** Whether the form submit button should be enabled when offline */ + enabledWhenOffline?: boolean; }; -function AddressStep({onNext, isEditing, shouldSaveDraft = false, shouldHideCountrySelector = false}: AddressStepProps) { +function AddressStep({onNext, isEditing, shouldSaveDraft = false, shouldHideCountrySelector = false, enabledWhenOffline}: AddressStepProps) { const styles = useThemeStyles(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); @@ -134,6 +137,7 @@ function AddressStep({onNext, isEditing, shouldSaveDraft = false, shouldHideCoun zip={zipcode} shouldSaveDraft={shouldSaveDraft} shouldHideCountrySelector={shouldHideCountrySelector} + enabledWhenOffline={enabledWhenOffline} /> ); diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx index f9c383e19c3b7..d92fb65fd8cb5 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx @@ -10,7 +10,12 @@ import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.BANK_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME]; -function LegalNameStep({onNext, onMove, isEditing}: SubStepProps) { +type LegalNameStepProps = SubStepProps & { + /** Whether the form submit button should be enabled when offline */ + enabledWhenOffline?: boolean; +}; + +function LegalNameStep({onNext, onMove, isEditing, enabledWhenOffline}: LegalNameStepProps) { const {translate} = useLocalize(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); @@ -39,6 +44,7 @@ function LegalNameStep({onNext, onMove, isEditing}: SubStepProps) { firstNameInputID={PERSONAL_INFO_STEP_KEY.FIRST_NAME} lastNameInputID={PERSONAL_INFO_STEP_KEY.LAST_NAME} defaultValues={defaultValues} + enabledWhenOffline={enabledWhenOffline} /> ); } diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx index 516a212a5739c..bf1609ee7ae2c 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx @@ -17,9 +17,12 @@ const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.PHONE_NUMBER]; type PhoneNumberStepProps = SubStepProps & { /** Whether to delay auto-focusing the input to avoid conflicts with navigation animations */ shouldDelayAutoFocus?: boolean; + + /** Whether the form submit button should be enabled when offline */ + enabledWhenOffline?: boolean; }; -function PhoneNumberStep({onNext, onMove, isEditing, shouldDelayAutoFocus}: PhoneNumberStepProps) { +function PhoneNumberStep({onNext, onMove, isEditing, shouldDelayAutoFocus, enabledWhenOffline = true}: PhoneNumberStepProps) { const {translate} = useLocalize(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); @@ -64,7 +67,7 @@ function PhoneNumberStep({onNext, onMove, isEditing, shouldDelayAutoFocus}: Phon inputMode={CONST.INPUT_MODE.TEL} defaultValue={defaultPhoneNumber} shouldDelayAutoFocus={shouldDelayAutoFocus} - enabledWhenOffline + enabledWhenOffline={enabledWhenOffline} /> ); } diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index e7e8578199b70..a239e546809a8 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -30,6 +30,18 @@ const PAGE_NAME = CONST.UPDATE_PERSONAL_BANK_ACCOUNT.PAGE_NAME; const PAGE_NAMES: string[] = [PAGE_NAME.LEGAL_NAME, PAGE_NAME.ADDRESS, PAGE_NAME.PHONE_NUMBER]; +function UpdateLegalName({isEditing, onNext, onMove}: SubStepProps) { + return ( + + ); +} +UpdateLegalName.displayName = 'UpdateLegalName'; + function AddressWithDraft({isEditing, onNext, onMove}: SubStepProps) { return (
); } @@ -50,13 +63,14 @@ function DelayedPhoneNumber({isEditing, onNext, onMove}: SubStepProps) { onNext={onNext} onMove={onMove} shouldDelayAutoFocus + enabledWhenOffline={false} /> ); } DelayedPhoneNumber.displayName = 'DelayedPhoneNumber'; const formPages = [ - {pageName: PAGE_NAME.LEGAL_NAME, component: LegalName}, + {pageName: PAGE_NAME.LEGAL_NAME, component: UpdateLegalName}, {pageName: PAGE_NAME.ADDRESS, component: AddressWithDraft}, {pageName: PAGE_NAME.PHONE_NUMBER, component: DelayedPhoneNumber}, ]; From 7cd6792d8a9e94fb12a0df6acdbb521eea85ef16 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Wed, 18 Mar 2026 22:59:17 +0300 Subject: [PATCH 61/63] chore: Remove testing mocks from BankAccountUtils --- src/libs/BankAccountUtils.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index d68fadea2009c..e73a0aa09e364 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -58,11 +58,6 @@ function hasOwnerPhone(additionalData: AdditionalData): boolean { * Check if a US personal bank account in OPEN state is missing required personal information. */ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): boolean { - // TODO: REMOVE MOCK - force missing info for testing - if (accountData?.bankAccountID === 8952303) { - return true; - } - if (accountData?.type !== CONST.BANK_ACCOUNT.TYPE.PERSONAL) { return false; } @@ -86,11 +81,6 @@ function isPersonalBankAccountMissingInfo(accountData: AccountData | undefined): * Returns step numbers that already have data on the bank account and can be skipped in the update flow. */ function getCompletedStepsForBankAccount(bankAccountList: OnyxEntry, bankAccountID: number): number[] { - // TODO: REMOVE MOCK - force all steps to show for testing - if (bankAccountID === 8952303) { - return []; - } - const bankAccount = bankAccountList?.[String(bankAccountID)]; if (!bankAccount) { return []; From a8f07d6f04a5cbe1ada9a12cf2c19b503b52bf1c Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 19 Mar 2026 09:00:03 +0300 Subject: [PATCH 62/63] fix: Remove duplicate error display - FormWrapper already reads from shared personalBankAccount key --- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index a239e546809a8..5b5353853d133 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -1,7 +1,6 @@ import React, {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; -import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -11,7 +10,6 @@ import useSubPage from '@hooks/useSubPage'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCompletedStepsForBankAccount} from '@libs/BankAccountUtils'; -import {getLatestErrorMessage} from '@libs/ErrorUtils'; import Log from '@libs/Log'; import {getCurrentAddress, getStreetLines} from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; @@ -98,7 +96,6 @@ function UpdatePersonalBankAccountPage() { const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const bankAccountID = personalBankAccount?.bankAccountID; - const errorMessage = getLatestErrorMessage(personalBankAccount ?? {}); useEffect(() => clearPersonalBankAccount, []); @@ -216,13 +213,6 @@ function UpdatePersonalBankAccountPage() { onNext={handleNextPage} onMove={() => {}} /> - {!!errorMessage && ( - - )} ); } From 05480f791bf281e622e1ae3ab93aead39379ee04 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Thu, 19 Mar 2026 09:19:41 +0300 Subject: [PATCH 63/63] fix: Add missing-info indicator to account tab and redirect when bankAccountID is missing --- src/hooks/useNavigationTabBarIndicatorChecks.ts | 4 ++-- .../settings/Wallet/UpdatePersonalBankAccountPage.tsx | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/hooks/useNavigationTabBarIndicatorChecks.ts b/src/hooks/useNavigationTabBarIndicatorChecks.ts index 63752960548ac..b5fc367b87a4f 100644 --- a/src/hooks/useNavigationTabBarIndicatorChecks.ts +++ b/src/hooks/useNavigationTabBarIndicatorChecks.ts @@ -2,7 +2,7 @@ import type {ValueOf} from 'type-fest'; import {isConnectionInProgress} from '@libs/actions/connections'; import {shouldShowQBOReimbursableExportDestinationAccountError} from '@libs/actions/connections/QuickbooksOnline'; import {hasPaymentMethodError} from '@libs/actions/PaymentMethods'; -import {hasPartiallySetupBankAccount} from '@libs/BankAccountUtils'; +import {hasPartiallySetupBankAccount, hasPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import {hasPendingExpensifyCardAction} from '@libs/CardUtils'; import {hasDomainErrors} from '@libs/DomainUtils'; import {getUberConnectionErrorDirectlyFromPolicy, shouldShowCustomUnitsError, shouldShowEmployeeListError, shouldShowPolicyError, shouldShowSyncError} from '@libs/PolicyUtils'; @@ -104,7 +104,7 @@ function useNavigationTabBarIndicatorChecks(): NavigationTabBarChecksResult { amountOwed, ownerBillingGraceEndPeriod, ), - [CONST.INDICATOR_STATUS.HAS_PARTIALLY_SETUP_BANK_ACCOUNT_INFO]: hasPartiallySetupBankAccount(bankAccountList), + [CONST.INDICATOR_STATUS.HAS_PARTIALLY_SETUP_BANK_ACCOUNT_INFO]: hasPartiallySetupBankAccount(bankAccountList) || hasPersonalBankAccountMissingInfo(bankAccountList), }; const domainChecks: Partial> = { diff --git a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx index 5b5353853d133..d3a3035757507 100644 --- a/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx +++ b/src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx @@ -91,14 +91,23 @@ function UpdatePersonalBankAccountPage() { const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); const [personalBankAccountDraft] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); const [homeAddressDraft] = useOnyx(ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT); - const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); + const [personalBankAccount, personalBankAccountResult] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const bankAccountID = personalBankAccount?.bankAccountID; + const isPersonalBankAccountLoaded = personalBankAccountResult.status === 'loaded'; useEffect(() => clearPersonalBankAccount, []); + useEffect(() => { + if (!isPersonalBankAccountLoaded || bankAccountID) { + return; + } + + Navigation.goBack(ROUTES.SETTINGS_WALLET); + }, [isPersonalBankAccountLoaded, bankAccountID]); + const completedSteps = bankAccountID ? getCompletedStepsForBankAccount(bankAccountList, bankAccountID) : []; const exitFlow = () => {