From aab19de8a1d7605489a4e41c62b1a545460c28af Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 19 Oct 2026 16:37:03 +0200 Subject: [PATCH 01/12] feat: show Wise global reimbursements menu item on Wallet behind beta --- src/pages/settings/Wallet/WalletPage/index.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 237a6636066f..506f2c55e657 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -30,6 +30,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaymentMethodState from '@hooks/usePaymentMethodState'; import type {FormattedSelectedPaymentMethod} from '@hooks/usePaymentMethodState/types'; +import usePermissions from '@hooks/usePermissions'; import {usePersonalDetailsByIDs} from '@hooks/usePersonalDetails'; import useRefreshPendingDigitalWalletApproval from '@hooks/useRefreshPendingDigitalWalletApproval'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -37,7 +38,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isPersonalBankAccountMissingInfo, showUnlockAlreadyRequestedModal} from '@libs/BankAccountUtils'; +import {isEligibleForWiseGlobalReimbursements, isPersonalBankAccountMissingInfo, showUnlockAlreadyRequestedModal} from '@libs/BankAccountUtils'; import {hasDisplayableAssignedCards, isDirectFeed, maskCardNumber} from '@libs/CardUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -112,6 +113,8 @@ function WalletPage() { const delegateAccountID = useDelegateAccountID(); const isUserValidated = userAccount?.validated ?? false; const {isAccountLocked} = useLockedAccountState(); + const {isBetaEnabled} = usePermissions(); + const isWiseGlobalReimbursementsEnabled = isBetaEnabled(CONST.BETAS.WISE_REIMBURSEMENTS); const {showLockedAccountModal} = useLockedAccountActions(); const {login: currentUserLogin, email, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {translate, localeCompare} = useLocalize(); @@ -469,11 +472,7 @@ function WalletPage() { const shouldShowEditNicknameButton = paymentMethod.selectedPaymentMethod?.state === CONST.BANK_ACCOUNT.STATE.OPEN; - const shouldShowEnableGlobalReimbursementsButton = - paymentMethod.selectedPaymentMethod?.additionalData?.currency === CONST.CURRENCY.USD && - paymentMethod.selectedPaymentMethod.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && - !paymentMethod.selectedPaymentMethod?.additionalData?.verifications?.corpay && - paymentMethod.selectedPaymentMethod?.state === CONST.BANK_ACCOUNT.STATE.OPEN; + const shouldShowEnableGlobalReimbursementsButton = isEligibleForWiseGlobalReimbursements(paymentMethod.selectedPaymentMethod); // Determines whether or not the modal popup is mounted from the bottom of the screen instead of the side mount on Web screen const alertTextStyle = [styles.inlineSystemMessage, styles.flexShrink1]; @@ -606,6 +605,10 @@ function WalletPage() { closeModal(() => showLockedAccountModal()); return; } + if (isWiseGlobalReimbursementsEnabled) { + closeModal(); + return; + } closeModal(() => Navigation.navigate( ROUTES.SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS_BUSINESS.getRoute( @@ -635,6 +638,7 @@ function WalletPage() { hasEligibleShareRecipient, shouldShowUnshareButton, shouldShowEnableGlobalReimbursementsButton, + isWiseGlobalReimbursementsEnabled, isAccountLocked, makeDefaultPaymentMethod, showLockedAccountModal, From a0686904ac56d148152383bea01e24b1944b6b9d Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 08:35:45 +0200 Subject: [PATCH 02/12] feat: open Wise intro page from Wallet global reimbursements menu item --- src/pages/settings/Wallet/WalletPage/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 506f2c55e657..827132387bd2 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -606,7 +606,7 @@ function WalletPage() { return; } if (isWiseGlobalReimbursementsEnabled) { - closeModal(); + closeModal(() => Navigation.navigate(ROUTES.SETTINGS_WALLET_WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(paymentMethod.selectedPaymentMethod.bankAccountID))); return; } closeModal(() => From f99679e0ddba71b74cac80d0718f453efa5e6cd3 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 09:32:02 +0200 Subject: [PATCH 03/12] fix: use Global reimbursements header on Wise intro page --- src/languages/en.ts | 1 + src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 237106085e90..e2d103720205 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2903,6 +2903,7 @@ 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', diff --git a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx index a98061f9a6c5..109a7f6f3cb0 100644 --- a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx +++ b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx @@ -67,7 +67,7 @@ function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntr >
- +
Date: Tue, 20 Oct 2026 10:16:10 +0200 Subject: [PATCH 04/12] chore: add Global reimbursements header translations --- src/languages/de.ts | 7 ++++++- src/languages/el.ts | 7 ++++++- src/languages/es.ts | 7 ++++++- src/languages/fr.ts | 1 + src/languages/it.ts | 7 ++++++- src/languages/ja.ts | 7 ++++++- src/languages/nl.ts | 7 ++++++- src/languages/pl.ts | 7 ++++++- src/languages/pt-BR.ts | 7 ++++++- src/languages/zh-hans.ts | 2 +- 10 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index aff85e69838e..800637e69aa6 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2834,7 +2834,12 @@ 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', + }, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Karte zu ${walletName} hinzufügen`, diff --git a/src/languages/el.ts b/src/languages/el.ts index cda27383978a..779da1f655f3 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -2890,7 +2890,12 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'Αυτός ο λογαριασμός χρειάζεται προσοχή', unlock: 'Ξεκλείδωμα', }, - wiseGlobalReimbursementsIntro: {title: 'Να ρυθμιστούν οι παγκόσμιες αποζημιώσεις;', subtitle: 'Πληρώστε μέλη παγκοσμίως στο τοπικό τους νόμισμα.', getStarted: 'Ξεκινήστε'}, + wiseGlobalReimbursementsIntro: { + title: 'Να ρυθμιστούν οι παγκόσμιες αποζημιώσεις;', + subtitle: 'Πληρώστε μέλη παγκοσμίως στο τοπικό τους νόμισμα.', + getStarted: 'Ξεκινήστε', + headerTitle: 'Παγκόσμιες αποζημιώσεις', + }, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Προσθήκη κάρτας στο ${walletName}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index d7ad5a7a7bd2..a4c76ab4e6c9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2744,7 +2744,12 @@ 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', + }, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Añadir tarjeta a ${walletName}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index a089625e7b51..725975184c1e 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2843,6 +2843,7 @@ 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', }, }, addCardToDigitalWallet: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 6eb2a2e9909e..0aad581fc818 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2823,7 +2823,12 @@ 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', + }, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Aggiungi carta a ${walletName}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 58a13bfcc91e..4bbdc29eddc1 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2802,7 +2802,12 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'このアカウントには対応が必要です', unlock: 'ロック解除', }, - wiseGlobalReimbursementsIntro: {title: 'グローバル払い戻しを設定しますか?', subtitle: '世界中のメンバーに現地通貨で支払えます。', getStarted: '使い始める'}, + wiseGlobalReimbursementsIntro: { + title: 'グローバル払い戻しを設定しますか?', + subtitle: '世界中のメンバーに現地通貨で支払えます。', + getStarted: '使い始める', + headerTitle: 'グローバル精算', + }, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `${walletName} にカードを追加`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 55d382cb1f87..2afdfbea8c4a 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2827,7 +2827,12 @@ 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', + }, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Kaart toevoegen aan ${walletName}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 17a89282fbea..84133cb3672a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2861,7 +2861,12 @@ 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', + }, }, 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..68a938c25ccb 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2818,7 +2818,12 @@ 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', + }, }, 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..83bad8d57dfa 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2728,7 +2728,7 @@ const translations: TranslationDeepObject = { accountRequiresAttention: '此账户需要处理', unlock: '解锁', }, - wiseGlobalReimbursementsIntro: {title: '设置全球报销?', subtitle: '按当地货币向全球成员付款。', getStarted: '开始使用'}, + wiseGlobalReimbursementsIntro: {title: '设置全球报销?', subtitle: '按当地货币向全球成员付款。', getStarted: '开始使用', headerTitle: '全球报销'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `将卡添加到 ${walletName}`, From 3176752f009dbb18711b0ddd0a9946c4414ee2da Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 10:51:40 +0200 Subject: [PATCH 05/12] feat: show Wise global reimbursements banner on Wallet behind beta --- src/CONST/index.ts | 5 ++ src/languages/en.ts | 4 + src/libs/BankAccountUtils.ts | 26 +++++++ .../settings/Wallet/WalletPage/index.tsx | 2 + .../Wallet/WiseGlobalReimbursementsBanner.tsx | 77 +++++++++++++++++++ .../WiseGlobalReimbursementsIntroPage.tsx | 6 +- src/types/onyx/DismissedProductTraining.ts | 5 ++ tests/unit/BankAccountUtilsTest.ts | 61 +++++++++++++++ 8 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx 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/en.ts b/src/languages/en.ts index e2d103720205..3db97d198f98 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2908,6 +2908,10 @@ const translations = { 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/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 0e9860c0f036..ce3a1fe7f2ef 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -148,6 +148,31 @@ 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 eligibleAccounts = Object.values(bankAccountList ?? {}) + .map((bankAccount) => bankAccount.accountData) + .filter((accountData): accountData is AccountData => isEligibleForWiseGlobalReimbursements(accountData) && !!accountData?.bankAccountID); + + const newestAccount = eligibleAccounts.reduce((newest, accountData) => { + if (!newest) { + return accountData; + } + // `created` is a DB timestamp, so plain string comparison orders it chronologically + const created = accountData.created ?? ''; + const newestCreated = newest.created ?? ''; + if (created !== newestCreated) { + return created > newestCreated ? accountData : newest; + } + // Accounts created in the same second fall back to the ID, which grows with each new account + return (accountData.bankAccountID ?? CONST.DEFAULT_NUMBER_ID) > (newest.bankAccountID ?? CONST.DEFAULT_NUMBER_ID) ? accountData : newest; + }, undefined); + + return newestAccount?.bankAccountID; +} + function doesPolicyHavePartiallySetupBankAccount(bankAccountList: OnyxEntry, policyID: string) { if (!bankAccountList) { return false; @@ -313,6 +338,7 @@ export { getBankAccountSearchLabel, isFilterableBankAccount, isEligibleForWiseGlobalReimbursements, + getWiseGlobalReimbursementsBannerBankAccountID, getDefaultCompanyWebsite, getBankAccountState, hasBankAccountAllowDebit, diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 827132387bd2..6e96f6b30c36 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -49,6 +49,7 @@ import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import {getFirstPageName} from '@pages/settings/Wallet/UpdatePersonalBankAccountPage'; +import WiseGlobalReimbursementsBanner from '@pages/settings/Wallet/WiseGlobalReimbursementsBanner'; import {deletePaymentBankAccount, openPersonalBankAccountSetupView, pressLockedBankAccount, resetPersonalBankAccountForUpdate} from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; @@ -769,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 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) || 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 109a7f6f3cb0..4b9898cbb7f6 100644 --- a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx +++ b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx @@ -11,6 +11,7 @@ import usePermissions from '@hooks/usePermissions'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import {dismissProductTraining} from '@libs/actions/Welcome'; import {isEligibleForWiseGlobalReimbursements} from '@libs/BankAccountUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -80,7 +81,10 @@ function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntr onSecondaryButtonPress={goBackToWallet} shouldShowButton buttonText={translate('walletPage.wiseGlobalReimbursementsIntro.getStarted')} - onButtonPress={() => {}} + 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..5896311cb27a 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -5,6 +5,7 @@ import { getDefaultCompanyWebsite, getLastFourDigits, getRequiredKYBDocuments, + getWiseGlobalReimbursementsBannerBankAccountID, hasBankAccountAllowDebit, hasPartiallySetupBankAccount, hasPersonalBankAccountMissingInfo, @@ -814,4 +815,64 @@ 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('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(); + }); + }); }); From c63c1ec81c1ee9fb7c40804c9d901433881a5a3c Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 10:56:34 +0200 Subject: [PATCH 06/12] chore: revert generated translations for non-English locales --- src/languages/de.ts | 7 +------ src/languages/el.ts | 7 +------ src/languages/es.ts | 7 +------ src/languages/fr.ts | 1 - src/languages/it.ts | 7 +------ src/languages/ja.ts | 7 +------ src/languages/nl.ts | 7 +------ src/languages/pl.ts | 7 +------ src/languages/pt-BR.ts | 7 +------ src/languages/zh-hans.ts | 2 +- 10 files changed, 9 insertions(+), 50 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 800637e69aa6..aff85e69838e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2834,12 +2834,7 @@ 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", - headerTitle: 'Globale Erstattungen', - }, + wiseGlobalReimbursementsIntro: {title: 'Globale Rückerstattungen einrichten?', subtitle: 'Zahlen Sie Mitglieder weltweit in ihrer lokalen Währung.', getStarted: "Los geht's"}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Karte zu ${walletName} hinzufügen`, diff --git a/src/languages/el.ts b/src/languages/el.ts index 779da1f655f3..cda27383978a 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -2890,12 +2890,7 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'Αυτός ο λογαριασμός χρειάζεται προσοχή', unlock: 'Ξεκλείδωμα', }, - wiseGlobalReimbursementsIntro: { - title: 'Να ρυθμιστούν οι παγκόσμιες αποζημιώσεις;', - subtitle: 'Πληρώστε μέλη παγκοσμίως στο τοπικό τους νόμισμα.', - getStarted: 'Ξεκινήστε', - headerTitle: 'Παγκόσμιες αποζημιώσεις', - }, + wiseGlobalReimbursementsIntro: {title: 'Να ρυθμιστούν οι παγκόσμιες αποζημιώσεις;', subtitle: 'Πληρώστε μέλη παγκοσμίως στο τοπικό τους νόμισμα.', getStarted: 'Ξεκινήστε'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Προσθήκη κάρτας στο ${walletName}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index a4c76ab4e6c9..d7ad5a7a7bd2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2744,12 +2744,7 @@ 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', - headerTitle: 'Reembolsos globales', - }, + wiseGlobalReimbursementsIntro: {title: '¿Configurar reembolsos globales?', subtitle: 'Paga a miembros de todo el mundo en su moneda local.', getStarted: 'Empieza'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Añadir tarjeta a ${walletName}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 725975184c1e..a089625e7b51 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2843,7 +2843,6 @@ 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', }, }, addCardToDigitalWallet: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 0aad581fc818..6eb2a2e9909e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2823,12 +2823,7 @@ 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', - headerTitle: 'Rimborsi globali', - }, + wiseGlobalReimbursementsIntro: {title: 'Configurare i rimborsi globali?', subtitle: 'Paga i membri in tutto il mondo nella loro valuta locale.', getStarted: 'Inizia'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Aggiungi carta a ${walletName}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 4bbdc29eddc1..58a13bfcc91e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2802,12 +2802,7 @@ const translations: TranslationDeepObject = { accountRequiresAttention: 'このアカウントには対応が必要です', unlock: 'ロック解除', }, - wiseGlobalReimbursementsIntro: { - title: 'グローバル払い戻しを設定しますか?', - subtitle: '世界中のメンバーに現地通貨で支払えます。', - getStarted: '使い始める', - headerTitle: 'グローバル精算', - }, + wiseGlobalReimbursementsIntro: {title: 'グローバル払い戻しを設定しますか?', subtitle: '世界中のメンバーに現地通貨で支払えます。', getStarted: '使い始める'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `${walletName} にカードを追加`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 2afdfbea8c4a..55d382cb1f87 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2827,12 +2827,7 @@ 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', - headerTitle: 'Wereldwijde terugbetalingen', - }, + wiseGlobalReimbursementsIntro: {title: 'Wereldwijde terugbetalingen instellen?', subtitle: 'Betaal leden wereldwijd in hun eigen lokale valuta.', getStarted: 'Aan de slag'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Kaart toevoegen aan ${walletName}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 84133cb3672a..17a89282fbea 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2861,12 +2861,7 @@ 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', - headerTitle: 'Globalne zwroty', - }, + wiseGlobalReimbursementsIntro: {title: 'Skonfigurować globalne zwroty kosztów?', subtitle: 'Płać członkom na całym świecie w ich lokalnej walucie.', getStarted: 'Zacznij'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Dodaj kartę do ${walletName}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 68a938c25ccb..274d62903144 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2818,12 +2818,7 @@ 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', - headerTitle: 'Reembolsos globais', - }, + wiseGlobalReimbursementsIntro: {title: 'Configurar reembolsos globais?', subtitle: 'Pague membros no mundo todo na moeda local deles.', getStarted: 'Começar'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `Adicionar cartão à ${walletName}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 83bad8d57dfa..79dafb4321f5 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2728,7 +2728,7 @@ const translations: TranslationDeepObject = { accountRequiresAttention: '此账户需要处理', unlock: '解锁', }, - wiseGlobalReimbursementsIntro: {title: '设置全球报销?', subtitle: '按当地货币向全球成员付款。', getStarted: '开始使用', headerTitle: '全球报销'}, + wiseGlobalReimbursementsIntro: {title: '设置全球报销?', subtitle: '按当地货币向全球成员付款。', getStarted: '开始使用'}, }, addCardToDigitalWallet: { title: ({walletName}: {walletName: string}) => `将卡添加到 ${walletName}`, From 25e0edb08075bd15f343650faa7801beca02ddbc Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 11:07:03 +0200 Subject: [PATCH 07/12] chore: add Wise global reimbursements translations --- src/languages/de.ts | 8 +++++++- src/languages/el.ts | 8 +++++++- src/languages/es.ts | 8 +++++++- src/languages/fr.ts | 2 ++ src/languages/it.ts | 8 +++++++- src/languages/ja.ts | 8 +++++++- src/languages/nl.ts | 8 +++++++- src/languages/pl.ts | 8 +++++++- src/languages/pt-BR.ts | 8 +++++++- src/languages/zh-hans.ts | 3 ++- 10 files changed, 60 insertions(+), 9 deletions(-) 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/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}`, From 2f887c5fa3466a2dfe4a0aad95c5e91111b06a88 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 11:44:45 +0200 Subject: [PATCH 08/12] refactor: simplify bank account ID tie-break in Wise banner picker --- src/libs/BankAccountUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index ce3a1fe7f2ef..6592a0b599f3 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -167,7 +167,9 @@ function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEnt return created > newestCreated ? accountData : newest; } // Accounts created in the same second fall back to the ID, which grows with each new account - return (accountData.bankAccountID ?? CONST.DEFAULT_NUMBER_ID) > (newest.bankAccountID ?? CONST.DEFAULT_NUMBER_ID) ? accountData : newest; + const accountID = accountData.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; + const newestAccountID = newest.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; + return accountID > newestAccountID ? accountData : newest; }, undefined); return newestAccount?.bankAccountID; From 1c8c677a4d2aa6dfc425501c027e7a817792d614 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 12:28:02 +0200 Subject: [PATCH 09/12] refactor: rename global reimbursements eligibility check and use sort in banner picker --- src/libs/BankAccountUtils.ts | 37 ++++++++----------- .../settings/Wallet/WalletPage/index.tsx | 4 +- .../WiseGlobalReimbursementsIntroPage.tsx | 4 +- tests/unit/BankAccountUtilsTest.ts | 18 ++++----- 4 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 6592a0b599f3..686258ede661 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 && @@ -154,25 +154,20 @@ function isEligibleForWiseGlobalReimbursements(accountData: AccountData | undefi function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEntry): number | undefined { const eligibleAccounts = Object.values(bankAccountList ?? {}) .map((bankAccount) => bankAccount.accountData) - .filter((accountData): accountData is AccountData => isEligibleForWiseGlobalReimbursements(accountData) && !!accountData?.bankAccountID); - - const newestAccount = eligibleAccounts.reduce((newest, accountData) => { - if (!newest) { - return accountData; - } - // `created` is a DB timestamp, so plain string comparison orders it chronologically - const created = accountData.created ?? ''; - const newestCreated = newest.created ?? ''; - if (created !== newestCreated) { - return created > newestCreated ? accountData : newest; - } - // Accounts created in the same second fall back to the ID, which grows with each new account - const accountID = accountData.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; - const newestAccountID = newest.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; - return accountID > newestAccountID ? accountData : newest; - }, undefined); - - return newestAccount?.bankAccountID; + .filter((accountData): accountData is AccountData => isBankAccountEligibleForGlobalReimbursements(accountData) && !!accountData?.bankAccountID); + + return eligibleAccounts + .sort((a, b) => { + // `created` is a DB timestamp, so plain string comparison orders it chronologically + const aCreated = a.created ?? ''; + const bCreated = b.created ?? ''; + if (aCreated !== bCreated) { + return bCreated > aCreated ? 1 : -1; + } + // Accounts created in the same second fall back to the ID, which grows with each new account + return (b.bankAccountID ?? CONST.DEFAULT_NUMBER_ID) - (a.bankAccountID ?? CONST.DEFAULT_NUMBER_ID); + }) + .at(0)?.bankAccountID; } function doesPolicyHavePartiallySetupBankAccount(bankAccountList: OnyxEntry, policyID: string) { @@ -339,7 +334,7 @@ function showUnlockAlreadyRequestedModal(showConfirmModal: ReturnType
diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 5896311cb27a..1e09c9d28631 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -10,7 +10,7 @@ import { hasPartiallySetupBankAccount, hasPersonalBankAccountMissingInfo, isBankAccountPartiallySetup, - isEligibleForWiseGlobalReimbursements, + isBankAccountEligibleForGlobalReimbursements, isPersonalBankAccountMissingInfo, isUserAddressVerificationRequired, isUserDOBVerificationRequired, @@ -224,7 +224,7 @@ describe('BankAccountUtils', () => { }); }); - describe('isEligibleForWiseGlobalReimbursements', () => { + describe('isBankAccountEligibleForGlobalReimbursements', () => { const eligibleAccountData: AccountData = { type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, state: CONST.BANK_ACCOUNT.STATE.OPEN, @@ -235,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])( @@ -279,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); }, ); }); From bf90ec8fa1a814a9d330cd27f4e3b80f3072cf24 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 12:29:43 +0200 Subject: [PATCH 10/12] fix: hide Wise global reimbursements banner in supportal and copilot sessions --- src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx b/src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx index 36d5bd6ee544..ad486468386b 100644 --- a/src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx +++ b/src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx @@ -6,6 +6,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useShouldSuppressPromotionalUI from '@hooks/useShouldSuppressPromotionalUI'; import useThemeStyles from '@hooks/useThemeStyles'; import {dismissProductTraining} from '@libs/actions/Welcome'; @@ -33,6 +34,7 @@ function WiseGlobalReimbursementsBanner() { 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); @@ -40,7 +42,7 @@ function WiseGlobalReimbursementsBanner() { const bankAccountID = getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList); - if (!isBetaEnabled(CONST.BETAS.WISE_REIMBURSEMENTS) || isDismissed || !bankAccountID) { + if (!isBetaEnabled(CONST.BETAS.WISE_REIMBURSEMENTS) || shouldSuppressPromotionalUI || isDismissed || !bankAccountID) { return null; } From e8f9e5b477d5d0f5f2fc45efde1fc3cef832c544 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 12:32:22 +0200 Subject: [PATCH 11/12] fix: skip pending-delete bank accounts in Wise global reimbursements banner --- src/libs/BankAccountUtils.ts | 2 ++ tests/unit/BankAccountUtilsTest.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 686258ede661..70e86e5895cf 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -153,6 +153,8 @@ function isBankAccountEligibleForGlobalReimbursements(accountData: AccountData | */ function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEntry): number | undefined { const eligibleAccounts = Object.values(bankAccountList ?? {}) + // Accounts deleted offline stay in the list until the request syncs, so they should not be offered + .filter((bankAccount) => bankAccount.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) .map((bankAccount) => bankAccount.accountData) .filter((accountData): accountData is AccountData => isBankAccountEligibleForGlobalReimbursements(accountData) && !!accountData?.bankAccountID); diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 1e09c9d28631..ee2b1e8078ee 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -852,6 +852,18 @@ describe('BankAccountUtils', () => { 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({ From d963a9a37558486b3873172dbc54b49efc022004 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 13:26:01 +0200 Subject: [PATCH 12/12] refactor: pick Wise banner bank account in a single reduce pass --- src/libs/BankAccountUtils.ts | 41 +++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index 70e86e5895cf..d335186f9693 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -152,24 +152,31 @@ function isBankAccountEligibleForGlobalReimbursements(accountData: AccountData | * Picks the bank account the Wallet global reimbursements banner points to: the most recently added eligible account. */ function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEntry): number | undefined { - const eligibleAccounts = Object.values(bankAccountList ?? {}) + 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 - .filter((bankAccount) => bankAccount.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) - .map((bankAccount) => bankAccount.accountData) - .filter((accountData): accountData is AccountData => isBankAccountEligibleForGlobalReimbursements(accountData) && !!accountData?.bankAccountID); - - return eligibleAccounts - .sort((a, b) => { - // `created` is a DB timestamp, so plain string comparison orders it chronologically - const aCreated = a.created ?? ''; - const bCreated = b.created ?? ''; - if (aCreated !== bCreated) { - return bCreated > aCreated ? 1 : -1; - } - // Accounts created in the same second fall back to the ID, which grows with each new account - return (b.bankAccountID ?? CONST.DEFAULT_NUMBER_ID) - (a.bankAccountID ?? CONST.DEFAULT_NUMBER_ID); - }) - .at(0)?.bankAccountID; + 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) {