diff --git a/src/CONST/index.ts b/src/CONST/index.ts index a295f031849a..4072e9f48acc 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -9378,6 +9378,7 @@ const CONST = { INBOX_TAB_STALE_UNREAD_MONTHS: 3, CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal', AGENTS_RULES_BANNER: 'agentsRulesBanner', + WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER: 'wiseGlobalReimbursementsWalletBanner', SMART_BANNER_HEIGHT: 152, NAVIGATION_TESTS: { @@ -9620,6 +9621,10 @@ const CONST = { CTA: 'AgentsRulesBanner-CTA', DISMISS: 'AgentsRulesBanner-Dismiss', }, + WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER: { + CTA: 'WiseGlobalReimbursementsWalletBanner-CTA', + DISMISS: 'WiseGlobalReimbursementsWalletBanner-Dismiss', + }, PRODUCT_MARKETING_WINDOW: { CTA: 'ProductMarketingWindow-CTA', DISMISS: 'ProductMarketingWindow-Dismiss', diff --git a/src/languages/de.ts b/src/languages/de.ts index aff85e69838e..85ff4ca47323 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2834,7 +2834,13 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'Dieses Konto erfordert Ihre Aufmerksamkeit', unlock: 'Entsperren', }, - wiseGlobalReimbursementsIntro: {title: 'Globale Rückerstattungen einrichten?', subtitle: 'Zahlen Sie Mitglieder weltweit in ihrer lokalen Währung.', getStarted: "Los geht's"}, + wiseGlobalReimbursementsIntro: { + title: 'Globale Rückerstattungen einrichten?', + subtitle: 'Zahlen Sie Mitglieder weltweit in ihrer lokalen Währung.', + getStarted: "Los geht's", + headerTitle: 'Globale Erstattungen', + }, + wiseGlobalReimbursementsBanner: {title: 'Weltweit bezahlen', subtitle: 'Aktivieren Sie globale Erstattungen auf Ihrem USD-Konto.'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Karte zu ${walletName} hinzufügen`, diff --git a/src/languages/el.ts b/src/languages/el.ts index cda27383978a..db192b155697 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -2890,7 +2890,13 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'Αυτός ο λογαριασμός χρειάζεται προσοχή', unlock: 'Ξεκλείδωμα', }, - wiseGlobalReimbursementsIntro: {title: 'Να ρυθμιστούν οι παγκόσμιες αποζημιώσεις;', subtitle: 'Πληρώστε μέλη παγκοσμίως στο τοπικό τους νόμισμα.', getStarted: 'Ξεκινήστε'}, + wiseGlobalReimbursementsIntro: { + title: 'Να ρυθμιστούν οι παγκόσμιες αποζημιώσεις;', + subtitle: 'Πληρώστε μέλη παγκοσμίως στο τοπικό τους νόμισμα.', + getStarted: 'Ξεκινήστε', + headerTitle: 'Παγκόσμιες αποζημιώσεις', + }, + wiseGlobalReimbursementsBanner: {title: 'Πληρώστε ανθρώπους παγκοσμίως', subtitle: 'Ενεργοποιήστε τις παγκόσμιες αποζημιώσεις στον λογαριασμό USD σας.'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Προσθήκη κάρτας στο ${walletName}`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 237106085e90..3db97d198f98 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2903,10 +2903,15 @@ const translations = { nickname: 'Nickname', editNicknameInstruction: 'Give the bank account a nickname that sets it apart from others.', wiseGlobalReimbursementsIntro: { + headerTitle: 'Global reimbursements', title: 'Set up global reimbursements?', subtitle: 'Pay members worldwide in their local currency.', getStarted: 'Get started', }, + wiseGlobalReimbursementsBanner: { + title: 'Pay people worldwide', + subtitle: 'Enable global reimbursements on your USD account.', + }, deleteCard: 'Delete card', deleteCardConfirmation: 'All unsubmitted card transactions, including those on open reports, will be removed. Are you sure you want to delete this card? You cannot undo this action.', diff --git a/src/languages/es.ts b/src/languages/es.ts index d7ad5a7a7bd2..b37b46625c8e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2744,7 +2744,13 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'Esta cuenta requiere atención', unlock: 'Desbloquear', }, - wiseGlobalReimbursementsIntro: {title: '¿Configurar reembolsos globales?', subtitle: 'Paga a miembros de todo el mundo en su moneda local.', getStarted: 'Empieza'}, + wiseGlobalReimbursementsIntro: { + title: '¿Configurar reembolsos globales?', + subtitle: 'Paga a miembros de todo el mundo en su moneda local.', + getStarted: 'Empieza', + headerTitle: 'Reembolsos globales', + }, + wiseGlobalReimbursementsBanner: {title: 'Paga a personas de todo el mundo', subtitle: 'Habilita los reembolsos globales en tu cuenta en USD.'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Añadir tarjeta a ${walletName}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index a089625e7b51..f42e28eac4a1 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2843,7 +2843,9 @@ const translations: TranslationDeepObject = { title: 'Configurer les remboursements globaux ?', subtitle: 'Payez des membres partout dans le monde dans leur devise locale.', getStarted: 'Commencer', + headerTitle: 'Remboursements globaux', }, + wiseGlobalReimbursementsBanner: {title: 'Payer des personnes dans le monde entier', subtitle: 'Activez les remboursements globaux sur votre compte en USD.'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Ajouter la carte à ${walletName}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 6eb2a2e9909e..c98a6d03573e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2823,7 +2823,13 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'Questo conto richiede attenzione', unlock: 'Sblocca', }, - wiseGlobalReimbursementsIntro: {title: 'Configurare i rimborsi globali?', subtitle: 'Paga i membri in tutto il mondo nella loro valuta locale.', getStarted: 'Inizia'}, + wiseGlobalReimbursementsIntro: { + title: 'Configurare i rimborsi globali?', + subtitle: 'Paga i membri in tutto il mondo nella loro valuta locale.', + getStarted: 'Inizia', + headerTitle: 'Rimborsi globali', + }, + wiseGlobalReimbursementsBanner: {title: 'Paga persone in tutto il mondo', subtitle: 'Abilita i rimborsi globali sul tuo conto in USD.'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Aggiungi carta a ${walletName}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 58a13bfcc91e..c34939dc788a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2802,7 +2802,13 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'このアカウントには対応が必要です', unlock: 'ロック解除', }, - wiseGlobalReimbursementsIntro: {title: 'グローバル払い戻しを設定しますか?', subtitle: '世界中のメンバーに現地通貨で支払えます。', getStarted: '使い始める'}, + wiseGlobalReimbursementsIntro: { + title: 'グローバル払い戻しを設定しますか?', + subtitle: '世界中のメンバーに現地通貨で支払えます。', + getStarted: '使い始める', + headerTitle: 'グローバル払い戻し', + }, + wiseGlobalReimbursementsBanner: {title: '世界中の人に支払いましょう', subtitle: 'USD アカウントでグローバル払い戻しを有効にします。'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `${walletName} にカードを追加`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 55d382cb1f87..aeb3da5401e6 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2827,7 +2827,13 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'Deze account vereist aandacht', unlock: 'Ontgrendelen', }, - wiseGlobalReimbursementsIntro: {title: 'Wereldwijde terugbetalingen instellen?', subtitle: 'Betaal leden wereldwijd in hun eigen lokale valuta.', getStarted: 'Aan de slag'}, + wiseGlobalReimbursementsIntro: { + title: 'Wereldwijde terugbetalingen instellen?', + subtitle: 'Betaal leden wereldwijd in hun eigen lokale valuta.', + getStarted: 'Aan de slag', + headerTitle: 'Wereldwijde terugbetalingen', + }, + wiseGlobalReimbursementsBanner: {title: 'Betaal mensen wereldwijd', subtitle: 'Schakel wereldwijde terugbetalingen in op je USD-rekening.'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Kaart toevoegen aan ${walletName}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 17a89282fbea..166cf3df4849 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2861,7 +2861,13 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'To konto wymaga uwagi', unlock: 'Odblokuj', }, - wiseGlobalReimbursementsIntro: {title: 'Skonfigurować globalne zwroty kosztów?', subtitle: 'Płać członkom na całym świecie w ich lokalnej walucie.', getStarted: 'Zacznij'}, + wiseGlobalReimbursementsIntro: { + title: 'Skonfigurować globalne zwroty kosztów?', + subtitle: 'Płać członkom na całym świecie w ich lokalnej walucie.', + getStarted: 'Zacznij', + headerTitle: 'Globalne zwroty kosztów', + }, + wiseGlobalReimbursementsBanner: {title: 'Płać ludziom na całym świecie', subtitle: 'Włącz globalne zwroty wydatków na swoim koncie w USD.'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Dodaj kartę do ${walletName}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 274d62903144..e597da953897 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2818,7 +2818,13 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'Esta conta precisa de atenção', unlock: 'Desbloquear', }, - wiseGlobalReimbursementsIntro: {title: 'Configurar reembolsos globais?', subtitle: 'Pague membros no mundo todo na moeda local deles.', getStarted: 'Começar'}, + wiseGlobalReimbursementsIntro: { + title: 'Configurar reembolsos globais?', + subtitle: 'Pague membros no mundo todo na moeda local deles.', + getStarted: 'Começar', + headerTitle: 'Reembolsos globais', + }, + wiseGlobalReimbursementsBanner: {title: 'Pague pessoas no mundo todo', subtitle: 'Ative reembolsos globais na sua conta em USD.'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Adicionar cartão à ${walletName}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 79dafb4321f5..dfaa3be77617 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2728,7 +2728,8 @@ const translations: TranslationDeepObject = { accountRequiresAttention: '此账户需要处理', unlock: '解锁', }, - wiseGlobalReimbursementsIntro: {title: '设置全球报销?', subtitle: '按当地货币向全球成员付款。', getStarted: '开始使用'}, + wiseGlobalReimbursementsIntro: {title: '设置全球报销?', subtitle: '按当地货币向全球成员付款。', getStarted: '开始使用', headerTitle: '全球报销'}, + wiseGlobalReimbursementsBanner: {title: '向全球付款', subtitle: '在您的美元账户中启用全球报销。'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `将卡添加到 ${walletName}`, diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 0e9860c0f036..d335186f9693 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -139,7 +139,7 @@ function isFilterableBankAccount(bankAccount: OnyxEntry): /** * A verified (OPEN) USD business bank account that is not already set up for global reimbursements can start the Wise flow. */ -function isEligibleForWiseGlobalReimbursements(accountData: AccountData | undefined): boolean { +function isBankAccountEligibleForGlobalReimbursements(accountData: AccountData | undefined): boolean { return ( accountData?.additionalData?.currency === CONST.CURRENCY.USD && accountData.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && @@ -148,6 +148,37 @@ function isEligibleForWiseGlobalReimbursements(accountData: AccountData | undefi ); } +/** + * Picks the bank account the Wallet global reimbursements banner points to: the most recently added eligible account. + */ +function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEntry): number | undefined { + const newest = Object.values(bankAccountList ?? {}).reduce((latest, bankAccount) => { + // Accounts deleted offline stay in the list until the request syncs, so they should not be offered + if (bankAccount.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return latest; + } + + const accountData = bankAccount.accountData; + if (!accountData?.bankAccountID || !isBankAccountEligibleForGlobalReimbursements(accountData)) { + return latest; + } + + if (!latest) { + return accountData; + } + + // `created` is a DB timestamp, so plain string comparison orders it chronologically. + // Accounts created in the same second fall back to the ID, which grows with each new account + const created = accountData.created ?? ''; + const latestCreated = latest.created ?? ''; + const isNewer = created > latestCreated || (created === latestCreated && accountData.bankAccountID > (latest.bankAccountID ?? CONST.DEFAULT_NUMBER_ID)); + + return isNewer ? accountData : latest; + }, undefined); + + return newest?.bankAccountID; +} + function doesPolicyHavePartiallySetupBankAccount(bankAccountList: OnyxEntry, policyID: string) { if (!bankAccountList) { return false; @@ -312,7 +343,8 @@ function showUnlockAlreadyRequestedModal(showConfirmModal: ReturnType showLockedAccountModal()); return; } + if (isWiseGlobalReimbursementsEnabled) { + closeModal(() => Navigation.navigate(ROUTES.SETTINGS_WALLET_WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(paymentMethod.selectedPaymentMethod.bankAccountID))); + return; + } closeModal(() => Navigation.navigate( ROUTES.SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS_BUSINESS.getRoute( @@ -635,6 +639,7 @@ function WalletPage() { hasEligibleShareRecipient, shouldShowUnshareButton, shouldShowEnableGlobalReimbursementsButton, + isWiseGlobalReimbursementsEnabled, isAccountLocked, makeDefaultPaymentMethod, showLockedAccountModal, @@ -765,6 +770,7 @@ function WalletPage() { errors={userWallet?.errors} errorRowStyles={styles.ph6} > +
): boolean => !!value?.[CONST.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER]; + +/** Promotes Wise global reimbursements on Wallet for the most recently added eligible bank account, until dismissed or onboarding starts */ +function WiseGlobalReimbursementsBanner() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {pageGutterMargin, cardGapBottom} = useLayoutSpacing(); + const {shouldUseNarrowLayout, isInLandscapeMode} = useResponsiveLayout(); + const {isBetaEnabled} = usePermissions(); + const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI(); + const illustrations = useMemoizedLazyIllustrations(['EarthWithCoins']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Close']); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [isDismissed = true] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {selector: isBannerDismissedSelector}); + + const bankAccountID = getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList); + + if (!isBetaEnabled(CONST.BETAS.WISE_REIMBURSEMENTS) || shouldSuppressPromotionalUI || isDismissed || !bankAccountID) { + return null; + } + + const shouldDisplayButtonInSeparateLine = shouldUseNarrowLayout && !isInLandscapeMode; + + return ( + + dismissProductTraining(CONST.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER, true)} + rightIconAccessibilityLabel={translate('common.dismiss')} + rightIconSentryLabel={CONST.SENTRY_LABEL.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER.DISMISS} + rightComponent={ + + + + } + /> + + ); +} + +export default WiseGlobalReimbursementsBanner; diff --git a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx index a98061f9a6c5..c62eab50b983 100644 --- a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx +++ b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx @@ -11,7 +11,8 @@ import usePermissions from '@hooks/usePermissions'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isEligibleForWiseGlobalReimbursements} from '@libs/BankAccountUtils'; +import {dismissProductTraining} from '@libs/actions/Welcome'; +import {isBankAccountEligibleForGlobalReimbursements} from '@libs/BankAccountUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import Navigation from '@navigation/Navigation'; @@ -62,12 +63,12 @@ function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntr return (
- +
{}} + onButtonPress={() => { + // Starting onboarding for any account retires the Wallet banner for good + dismissProductTraining(CONST.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER); + }} />
diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts index d53bf656f8b8..f0e4bbb39ca9 100644 --- a/src/types/onyx/DismissedProductTraining.ts +++ b/src/types/onyx/DismissedProductTraining.ts @@ -72,6 +72,11 @@ type DismissedProductTraining = { */ [CONST.AGENTS_RULES_BANNER]: DismissedProductTrainingElement; + /** + * When user dismisses the Wallet global reimbursements banner, or starts global reimbursements onboarding, we store the timestamp here. + */ + [CONST.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER]: DismissedProductTrainingElement; + /** * When user dismisses the GPS tooltip, we store the timestamp here. */ diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index c9e16023cec4..ee2b1e8078ee 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -5,11 +5,12 @@ import { getDefaultCompanyWebsite, getLastFourDigits, getRequiredKYBDocuments, + getWiseGlobalReimbursementsBannerBankAccountID, hasBankAccountAllowDebit, hasPartiallySetupBankAccount, hasPersonalBankAccountMissingInfo, isBankAccountPartiallySetup, - isEligibleForWiseGlobalReimbursements, + isBankAccountEligibleForGlobalReimbursements, isPersonalBankAccountMissingInfo, isUserAddressVerificationRequired, isUserDOBVerificationRequired, @@ -223,7 +224,7 @@ describe('BankAccountUtils', () => { }); }); - describe('isEligibleForWiseGlobalReimbursements', () => { + describe('isBankAccountEligibleForGlobalReimbursements', () => { const eligibleAccountData: AccountData = { type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, state: CONST.BANK_ACCOUNT.STATE.OPEN, @@ -234,42 +235,42 @@ describe('BankAccountUtils', () => { // Given a verified USD business bank account // When checking whether it can start the Wise global reimbursements flow // Then it is eligible, since that is the only kind of account Wise onboarding supports - expect(isEligibleForWiseGlobalReimbursements(eligibleAccountData)).toBe(true); + expect(isBankAccountEligibleForGlobalReimbursements(eligibleAccountData)).toBe(true); }); it('returns false when the account data is missing', () => { // Given no account data, e.g. an unknown bankAccountID in the URL // When checking eligibility // Then it is not eligible, so the intro page shows not-found instead of crashing - expect(isEligibleForWiseGlobalReimbursements(undefined)).toBe(false); + expect(isBankAccountEligibleForGlobalReimbursements(undefined)).toBe(false); }); it('returns false for a personal account', () => { // Given a personal account that is otherwise eligible // When checking eligibility // Then it is not eligible, because global reimbursements are paid from business accounts only - expect(isEligibleForWiseGlobalReimbursements({...eligibleAccountData, type: CONST.BANK_ACCOUNT.TYPE.PERSONAL})).toBe(false); + expect(isBankAccountEligibleForGlobalReimbursements({...eligibleAccountData, type: CONST.BANK_ACCOUNT.TYPE.PERSONAL})).toBe(false); }); it('returns false for a non-USD account', () => { // Given a business account in a currency other than USD // When checking eligibility // Then it is not eligible, because Release 1 only supports U.S. accounts - expect(isEligibleForWiseGlobalReimbursements({...eligibleAccountData, additionalData: {currency: CONST.CURRENCY.EUR}})).toBe(false); + expect(isBankAccountEligibleForGlobalReimbursements({...eligibleAccountData, additionalData: {currency: CONST.CURRENCY.EUR}})).toBe(false); }); it('returns false when the currency is missing', () => { // Given a business account without a currency // When checking eligibility // Then it is not eligible, matching the existing Corpay condition that requires an explicit USD currency - expect(isEligibleForWiseGlobalReimbursements({...eligibleAccountData, additionalData: {}})).toBe(false); + expect(isBankAccountEligibleForGlobalReimbursements({...eligibleAccountData, additionalData: {}})).toBe(false); }); it('returns false when the account is already set up with Corpay', () => { // Given a verified USD business account that already has a Corpay verification // When checking eligibility // Then it is not eligible, because global reimbursements are already enabled for it, matching the Wallet menu condition - expect(isEligibleForWiseGlobalReimbursements({...eligibleAccountData, additionalData: {currency: CONST.CURRENCY.USD, verifications: {corpay: {}}}})).toBe(false); + expect(isBankAccountEligibleForGlobalReimbursements({...eligibleAccountData, additionalData: {currency: CONST.CURRENCY.USD, verifications: {corpay: {}}}})).toBe(false); }); it.each([CONST.BANK_ACCOUNT.STATE.SETUP, CONST.BANK_ACCOUNT.STATE.PENDING, CONST.BANK_ACCOUNT.STATE.VERIFYING, CONST.BANK_ACCOUNT.STATE.LOCKED])( @@ -278,7 +279,7 @@ describe('BankAccountUtils', () => { // Given a USD business account that is not verified yet or is locked // When checking eligibility // Then it is not eligible, because only OPEN accounts can send reimbursements - expect(isEligibleForWiseGlobalReimbursements({...eligibleAccountData, state})).toBe(false); + expect(isBankAccountEligibleForGlobalReimbursements({...eligibleAccountData, state})).toBe(false); }, ); }); @@ -814,4 +815,76 @@ describe('BankAccountUtils', () => { ]); }); }); + + describe('getWiseGlobalReimbursementsBannerBankAccountID', () => { + const buildBankAccount = (bankAccountID: number, created: string, overrides: Partial = {}) => ({ + accountData: { + bankAccountID, + created, + type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + additionalData: {currency: CONST.CURRENCY.USD}, + ...overrides, + }, + }); + + it('returns the most recently added eligible account', () => { + // Given two eligible accounts where the lower ID was added later + const bankAccountList = createMock({ + accountOne: buildBankAccount(1, '2026-05-01 10:00:00'), + accountTwo: buildBankAccount(2, '2026-03-01 10:00:00'), + }); + + // When picking the account for the Wallet banner + // Then the newest one by creation date wins, since the spec defaults the banner to the most recently added account + expect(getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList)).toBe(1); + }); + + it('skips accounts that are not eligible', () => { + // Given a newer personal account and an older eligible business account + const bankAccountList = createMock({ + accountOne: buildBankAccount(1, '2026-03-01 10:00:00'), + accountTwo: buildBankAccount(2, '2026-05-01 10:00:00', {type: CONST.BANK_ACCOUNT.TYPE.PERSONAL}), + }); + + // When picking the account for the Wallet banner + // Then only the eligible account is considered, so the banner never opens an intro page that would show not-found + expect(getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList)).toBe(1); + }); + + it('skips accounts pending deletion', () => { + // Given a newer eligible account deleted offline and an older eligible account + const bankAccountList = createMock({ + accountOne: buildBankAccount(1, '2026-03-01 10:00:00'), + accountTwo: {...buildBankAccount(2, '2026-05-01 10:00:00'), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + }); + + // When picking the account for the Wallet banner + // Then the pending-delete account is ignored, so the banner never points to an account queued for removal + expect(getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList)).toBe(1); + }); + + it('falls back to the higher ID when creation dates match', () => { + // Given two eligible accounts created at the same time + const bankAccountList = createMock({ + accountOne: buildBankAccount(1, '2026-05-01 10:00:00'), + accountTwo: buildBankAccount(2, '2026-05-01 10:00:00'), + }); + + // When picking the account for the Wallet banner + // Then the higher ID wins, since IDs grow with each newly added account + expect(getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList)).toBe(2); + }); + + it('returns undefined when no account is eligible', () => { + // Given only an account already set up for global reimbursements + const bankAccountList = createMock({ + accountOne: buildBankAccount(1, '2026-05-01 10:00:00', {additionalData: {currency: CONST.CURRENCY.USD, verifications: {corpay: {}}}}), + }); + + // When picking the account for the Wallet banner + // Then nothing is returned, so the banner stays hidden + expect(getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList)).toBeUndefined(); + }); + }); });