From 6db0e50ff2f810f1ca925d676221fef9a94ad328 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 15:26:03 +0200 Subject: [PATCH 1/6] feat: add Wise global reimbursements banner to Workspace Workflows payments --- src/CONST/index.ts | 5 ++ src/ROUTES.ts | 9 +++ src/SCREENS.ts | 1 + .../WiseGlobalReimbursementsBanner.tsx | 62 +++++++++++++++ src/libs/BankAccountUtils.ts | 29 ++++++- .../ModalStackNavigators/index.tsx | 1 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 4 + .../settings/Wallet/WalletPage/index.tsx | 4 +- .../WalletWiseGlobalReimbursementsBanner.tsx | 49 ++++++++++++ .../Wallet/WiseGlobalReimbursementsBanner.tsx | 79 ------------------- .../WiseGlobalReimbursementsIntroPage.tsx | 23 +++--- ...orkspaceWiseGlobalReimbursementsBanner.tsx | 59 ++++++++++++++ .../workflows/tabs/WorkflowsPaymentsTab.tsx | 3 + src/types/onyx/DismissedProductTraining.ts | 5 ++ tests/unit/BankAccountUtilsTest.ts | 67 +++++++++++++++- 17 files changed, 310 insertions(+), 94 deletions(-) create mode 100644 src/components/WiseGlobalReimbursementsBanner.tsx create mode 100644 src/pages/settings/Wallet/WalletWiseGlobalReimbursementsBanner.tsx delete mode 100644 src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx create mode 100644 src/pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 4072e9f48acc..1451f8d2268b 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -9379,6 +9379,7 @@ const CONST = { CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal', AGENTS_RULES_BANNER: 'agentsRulesBanner', WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER: 'wiseGlobalReimbursementsWalletBanner', + WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_BANNER: 'wiseGlobalReimbursementsWorkspaceBanner', SMART_BANNER_HEIGHT: 152, NAVIGATION_TESTS: { @@ -9625,6 +9626,10 @@ const CONST = { CTA: 'WiseGlobalReimbursementsWalletBanner-CTA', DISMISS: 'WiseGlobalReimbursementsWalletBanner-Dismiss', }, + WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_BANNER: { + CTA: 'WiseGlobalReimbursementsWorkspaceBanner-CTA', + DISMISS: 'WiseGlobalReimbursementsWorkspaceBanner-Dismiss', + }, PRODUCT_MARKETING_WINDOW: { CTA: 'ProductMarketingWindow-CTA', DISMISS: 'ProductMarketingWindow-Dismiss', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 51eba403a336..47554e000b7a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3470,6 +3470,15 @@ const ROUTES = { return `workspaces/${policyID}/workflows/currency-conversion-fees` as const; }, }, + WORKSPACE_WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO: { + route: 'workspaces/:policyID/workflows/global-reimbursements/:bankAccountID', + getRoute: (policyID: string | undefined, bankAccountID: number | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the WORKSPACE_WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO route'); + } + return `workspaces/${policyID}/workflows/global-reimbursements/${bankAccountID}` as const; + }, + }, WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET: { route: 'workspaces/:policyID/workflows/auto-reporting-frequency/monthly-offset', getRoute: (policyID: string | undefined) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 0f66fb4e8512..1371a4304a3d 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -927,6 +927,7 @@ const SCREENS = { WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', WORKFLOWS_CURRENCY_CONVERSION_FEES: 'Workspace_Workflows_Currency_Conversion_Fees', + WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO: 'Workspace_Workflows_Wise_Global_Reimbursements_Intro', DESCRIPTION: 'Workspace_Overview_Description', SHARE: 'Workspace_Overview_Share', NAME: 'Workspace_Overview_Name', diff --git a/src/components/WiseGlobalReimbursementsBanner.tsx b/src/components/WiseGlobalReimbursementsBanner.tsx new file mode 100644 index 000000000000..2523b81810a9 --- /dev/null +++ b/src/components/WiseGlobalReimbursementsBanner.tsx @@ -0,0 +1,62 @@ +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import BillingBanner from '@pages/settings/Subscription/CardSection/BillingBanner/BillingBanner'; + +import CONST from '@src/CONST'; + +import React from 'react'; +import {View} from 'react-native'; + +import Button from './Button'; + +type WiseGlobalReimbursementsBannerProps = { + /** Opens the global reimbursements intro page */ + onGetStartedPress: () => void; + + /** Hides the banner for good */ + onDismissPress: () => void; + + /** Sentry labels of the "Get started" and close buttons */ + sentryLabels: {CTA: string; DISMISS: string}; +}; + +/** The "Pay people worldwide" banner promoting Wise global reimbursements. Callers decide when it shows and what its buttons do */ +function WiseGlobalReimbursementsBanner({onGetStartedPress, onDismissPress, sentryLabels}: WiseGlobalReimbursementsBannerProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {shouldUseNarrowLayout, isInLandscapeMode} = useResponsiveLayout(); + const illustrations = useMemoizedLazyIllustrations(['EarthWithCoins']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Close']); + + const shouldDisplayButtonInSeparateLine = shouldUseNarrowLayout && !isInLandscapeMode; + + return ( + + + + } + /> + ); +} + +export default WiseGlobalReimbursementsBanner; diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index d335186f9693..ad09a2c5c952 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -153,7 +153,7 @@ function isBankAccountEligibleForGlobalReimbursements(accountData: AccountData | */ 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 + // Accounts deleted offline stay in the list until the request syncs if (bankAccount.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return latest; } @@ -167,8 +167,7 @@ function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEnt 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 + // `created` is a DB timestamp, so string compare works. Same second → higher ID wins const created = accountData.created ?? ''; const latestCreated = latest.created ?? ''; const isNewer = created > latestCreated || (created === latestCreated && accountData.bankAccountID > (latest.bankAccountID ?? CONST.DEFAULT_NUMBER_ID)); @@ -179,6 +178,29 @@ function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEnt return newest?.bankAccountID; } +/** + * Picks the bank account the Workspace Workflows global reimbursements banner points to: the workspace's connected account, when it is eligible. + */ +function getWorkspaceWiseGlobalReimbursementsBannerBankAccountID(policy: OnyxEntry, bankAccountList: OnyxEntry): number | undefined { + // Only "Reimburse via Expensify" pays out of the connected account, so the other choices have nothing to enable + if (policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) { + return undefined; + } + + const bankAccountID = policy.achAccount?.bankAccountID; + if (!bankAccountID) { + return undefined; + } + + // Admins without a copy of the account can't see its currency, type or verifications, so the banner stays hidden for them + const bankAccount = bankAccountList?.[bankAccountID]; + if (bankAccount?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isBankAccountEligibleForGlobalReimbursements(bankAccount?.accountData)) { + return undefined; + } + + return bankAccountID; +} + function doesPolicyHavePartiallySetupBankAccount(bankAccountList: OnyxEntry, policyID: string) { if (!bankAccountList) { return false; @@ -345,6 +367,7 @@ export { isFilterableBankAccount, isBankAccountEligibleForGlobalReimbursements, getWiseGlobalReimbursementsBannerBankAccountID, + getWorkspaceWiseGlobalReimbursementsBannerBankAccountID, getDefaultCompanyWebsite, getBankAccountState, hasBankAccountAllowDebit, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c15230f8ae1d..369081256599 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1086,6 +1086,7 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/reconciliation/DynamicReconciliationFeedSelectorPage').default, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default, [SCREENS.WORKSPACE.WORKFLOWS_CURRENCY_CONVERSION_FEES]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsCurrencyConversionFeesPage').default, + [SCREENS.WORKSPACE.WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: () => require('../../../../pages/settings/Wallet/WiseGlobalReimbursementsIntroPage').default, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default, [SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../../pages/workspace/taxes/WorkspaceEditTaxPage').default, [SCREENS.WORKSPACE.TAX_NAME]: () => require('../../../../pages/workspace/taxes/NamePage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 62623bcbfc5e..fd6680c48ddb 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -51,6 +51,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.WORKFLOWS_CURRENCY_CONVERSION_FEES]: { path: ROUTES.WORKSPACE_WORKFLOWS_CURRENCY_CONVERSION_FEES.route, }, + [SCREENS.WORKSPACE.WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: { + path: ROUTES.WORKSPACE_WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO.route, + }, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: { path: ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3774fd074252..9531cf7bf48c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1149,6 +1149,10 @@ type WorkspaceSplitNavigatorParamList = { [SCREENS.WORKSPACE.WORKFLOWS_CURRENCY_CONVERSION_FEES]: { policyID: string; }; + [SCREENS.WORKSPACE.WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: { + policyID: string; + bankAccountID: string; + }; [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: { policyID: string; }; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 2dbc785da2ac..57a5682511a8 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -49,7 +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 WalletWiseGlobalReimbursementsBanner from '@pages/settings/Wallet/WalletWiseGlobalReimbursementsBanner'; import {deletePaymentBankAccount, openPersonalBankAccountSetupView, pressLockedBankAccount, resetPersonalBankAccountForUpdate} from '@userActions/BankAccounts'; import {deletePersonalCard} from '@userActions/Card'; @@ -770,7 +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 WalletWiseGlobalReimbursementsBanner() { + const {pageGutterMargin, cardGapBottom} = useLayoutSpacing(); + const {isBetaEnabled} = usePermissions(); + const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI(); + 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; + } + + return ( + + Navigation.navigate(ROUTES.SETTINGS_WALLET_WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(bankAccountID))} + onDismissPress={() => dismissProductTraining(CONST.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER, true)} + sentryLabels={CONST.SENTRY_LABEL.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER} + /> + + ); +} + +export default WalletWiseGlobalReimbursementsBanner; diff --git a/src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx b/src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx deleted file mode 100644 index ad486468386b..000000000000 --- a/src/pages/settings/Wallet/WiseGlobalReimbursementsBanner.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import Button from '@components/Button'; - -import useLayoutSpacing from '@hooks/useLayoutSpacing'; -import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; -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'; -import {getWiseGlobalReimbursementsBannerBankAccountID} from '@libs/BankAccountUtils'; -import Navigation from '@libs/Navigation/Navigation'; - -import BillingBanner from '@pages/settings/Subscription/CardSection/BillingBanner/BillingBanner'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type {DismissedProductTraining} from '@src/types/onyx'; - -import type {OnyxEntry} from 'react-native-onyx'; - -import React from 'react'; -import {View} from 'react-native'; - -const isBannerDismissedSelector = (value: OnyxEntry): 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 c62eab50b983..befed818e420 100644 --- a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx +++ b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx @@ -16,7 +16,7 @@ import {isBankAccountEligibleForGlobalReimbursements} from '@libs/BankAccountUti import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import Navigation from '@navigation/Navigation'; -import type {SettingsNavigatorParamList} from '@navigation/types'; +import type {SettingsNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; @@ -28,10 +28,14 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import React, {useEffect} from 'react'; -type WiseGlobalReimbursementsIntroPageProps = PlatformStackScreenProps; +type WiseGlobalReimbursementsIntroPageProps = + | PlatformStackScreenProps + | PlatformStackScreenProps; function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntroPageProps) { const bankAccountID = Number(route.params.bankAccountID); + // Opened from Workspace > Workflows > Payments when the route carries a workspace, from Wallet otherwise + const policyID = 'policyID' in route.params ? route.params.policyID : undefined; const [bankAccountList, bankAccountListMetadata] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [isLoadingPaymentMethods = true] = useOnyx(ONYXKEYS.IS_LOADING_PAYMENT_METHODS); const bankAccount = bankAccountList?.[bankAccountID]; @@ -43,15 +47,15 @@ function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntr const StyleUtils = useStyleUtils(); const illustrations = useMemoizedLazyIllustrations(['EarthWithCoins']); - const goBackToWallet = () => Navigation.goBack(ROUTES.SETTINGS_WALLET); + const goBackToEntryPoint = () => Navigation.goBack(policyID ? ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID, CONST.TAB.WORKFLOWS.PAYMENTS) : ROUTES.SETTINGS_WALLET); // Close the page instead of showing "not found" when the user lacks the beta. Wait for betas to load first useEffect(() => { if (isWiseReimbursementsBetaEnabled !== false) { return; } - goBackToWallet(); - }, [isWiseReimbursementsBetaEnabled, goBackToWallet]); + goBackToEntryPoint(); + }, [isWiseReimbursementsBetaEnabled, goBackToEntryPoint]); // On a cold direct link the account list can be empty until the payment-method fetch finishes, so wait for it before judging eligibility const isWaitingForBankAccount = !bankAccount && !!isLoadingPaymentMethods; @@ -64,10 +68,10 @@ function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntr
- +
{ - // Starting onboarding for any account retires the Wallet banner for good + // Starting onboarding for any account retires both banners for good dismissProductTraining(CONST.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER); + dismissProductTraining(CONST.WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_BANNER); }} />
diff --git a/src/pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner.tsx b/src/pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner.tsx new file mode 100644 index 000000000000..0764f36a41bb --- /dev/null +++ b/src/pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner.tsx @@ -0,0 +1,59 @@ +import WiseGlobalReimbursementsBanner from '@components/WiseGlobalReimbursementsBanner'; + +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; +import usePolicy from '@hooks/usePolicy'; +import useShouldSuppressPromotionalUI from '@hooks/useShouldSuppressPromotionalUI'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {dismissProductTraining} from '@libs/actions/Welcome'; +import {getWorkspaceWiseGlobalReimbursementsBannerBankAccountID} from '@libs/BankAccountUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {isPolicyAdmin} from '@libs/PolicyUtils'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {DismissedProductTraining} from '@src/types/onyx'; + +import type {OnyxEntry} from 'react-native-onyx'; + +import React from 'react'; +import {View} from 'react-native'; + +type WorkspaceWiseGlobalReimbursementsBannerProps = { + /** The workspace whose connected bank account the banner promotes */ + policyID: string; +}; + +const isBannerDismissedSelector = (value: OnyxEntry): boolean => !!value?.[CONST.WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_BANNER]; + +/** Promotes Wise global reimbursements in Workflows > Payments for the workspace's connected bank account, until dismissed or onboarding starts */ +function WorkspaceWiseGlobalReimbursementsBanner({policyID}: WorkspaceWiseGlobalReimbursementsBannerProps) { + const styles = useThemeStyles(); + const policy = usePolicy(policyID); + const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + const {isBetaEnabled} = usePermissions(); + const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI(); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [isDismissed = true] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {selector: isBannerDismissedSelector}); + + const bankAccountID = getWorkspaceWiseGlobalReimbursementsBannerBankAccountID(policy, bankAccountList); + + if (!isBetaEnabled(CONST.BETAS.WISE_REIMBURSEMENTS) || !isPolicyAdmin(policy, currentUserLogin) || shouldSuppressPromotionalUI || isDismissed || !bankAccountID) { + return null; + } + + return ( + + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(policyID, bankAccountID))} + onDismissPress={() => dismissProductTraining(CONST.WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_BANNER, true)} + sentryLabels={CONST.SENTRY_LABEL.WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_BANNER} + /> + + ); +} + +export default WorkspaceWiseGlobalReimbursementsBanner; diff --git a/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx b/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx index 636cadbe0751..874ac7debe0b 100644 --- a/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx +++ b/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx @@ -36,6 +36,8 @@ import {getReimbursementChoice, isPolicyAdmin, isSubmitPolicy} from '@libs/Polic import {hasInProgressVBBA} from '@libs/ReimbursementAccountUtils'; import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; +import WorkspaceWiseGlobalReimbursementsBanner from '@pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner'; + import {pressLockedBankAccount} from '@userActions/BankAccounts'; import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; import {navigateToConciergeChat} from '@userActions/Report'; @@ -418,6 +420,7 @@ function WorkflowsPaymentsTab({policyID}: WorkflowsPaymentsTabProps) { )} + } isActive={getReimbursementChoice(policy) !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO} diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts index f0e4bbb39ca9..b9cb49fda92a 100644 --- a/src/types/onyx/DismissedProductTraining.ts +++ b/src/types/onyx/DismissedProductTraining.ts @@ -77,6 +77,11 @@ type DismissedProductTraining = { */ [CONST.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER]: DismissedProductTrainingElement; + /** + * When user dismisses the Workspace Workflows global reimbursements banner, or starts global reimbursements onboarding, we store the timestamp here. + */ + [CONST.WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_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 ee2b1e8078ee..26d4524e5287 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -6,6 +6,7 @@ import { getLastFourDigits, getRequiredKYBDocuments, getWiseGlobalReimbursementsBannerBankAccountID, + getWorkspaceWiseGlobalReimbursementsBannerBankAccountID, hasBankAccountAllowDebit, hasPartiallySetupBankAccount, hasPersonalBankAccountMissingInfo, @@ -20,7 +21,7 @@ import type {KYBVerificationResponses} from '@libs/BankAccountUtils'; import CONST from '@src/CONST'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import type {Account, BankAccountList, Session} from '@src/types/onyx'; +import type {Account, BankAccountList, Policy, Session} from '@src/types/onyx'; import type AccountData from '@src/types/onyx/AccountData'; import createMock from '../utils/createMock'; @@ -887,4 +888,68 @@ describe('BankAccountUtils', () => { expect(getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList)).toBeUndefined(); }); }); + + describe('getWorkspaceWiseGlobalReimbursementsBannerBankAccountID', () => { + const bankAccountID = 1; + const eligibleAccountData: AccountData = { + bankAccountID, + type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + additionalData: {currency: CONST.CURRENCY.USD}, + }; + const policy = createMock({ + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + achAccount: {bankAccountID, accountNumber: '', routingNumber: '', addressName: '', bankName: '', reimburser: ''}, + }); + + it('returns the connected account when it is eligible', () => { + // Given a workspace reimbursing via Expensify from an eligible account the admin holds a copy of + const bankAccountList = createMock({[bankAccountID]: {accountData: eligibleAccountData}}); + + // When picking the account for the Workspace banner + // Then the connected account is returned, so the banner opens the intro page for it + expect(getWorkspaceWiseGlobalReimbursementsBannerBankAccountID(policy, bankAccountList)).toBe(bankAccountID); + }); + + it('returns undefined when the workspace does not reimburse via Expensify', () => { + // Given a workspace that tracks payments made elsewhere + const manualPolicy = createMock({...policy, reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL}); + const bankAccountList = createMock({[bankAccountID]: {accountData: eligibleAccountData}}); + + // When picking the account for the Workspace banner + // Then nothing is returned, since the connected account is not used to pay anyone + expect(getWorkspaceWiseGlobalReimbursementsBannerBankAccountID(manualPolicy, bankAccountList)).toBeUndefined(); + }); + + it('returns undefined when the admin does not hold a copy of the account', () => { + // Given an eligible connected account that is missing from the admin's bank account list + const bankAccountList = createMock({}); + + // When picking the account for the Workspace banner + // Then nothing is returned, since eligibility can't be checked without the account data + expect(getWorkspaceWiseGlobalReimbursementsBannerBankAccountID(policy, bankAccountList)).toBeUndefined(); + }); + + it('returns undefined when the connected account is not eligible', () => { + // Given a connected account that is already set up with Corpay + const bankAccountList = createMock({ + [bankAccountID]: {accountData: {...eligibleAccountData, additionalData: {currency: CONST.CURRENCY.USD, verifications: {corpay: {}}}}}, + }); + + // When picking the account for the Workspace banner + // Then nothing is returned, so the banner never opens an intro page that would show not-found + expect(getWorkspaceWiseGlobalReimbursementsBannerBankAccountID(policy, bankAccountList)).toBeUndefined(); + }); + + it('returns undefined when the connected account is pending deletion', () => { + // Given an eligible connected account deleted while offline + const bankAccountList = createMock({ + [bankAccountID]: {accountData: eligibleAccountData, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + }); + + // When picking the account for the Workspace banner + // Then nothing is returned, so the banner never points to an account queued for removal + expect(getWorkspaceWiseGlobalReimbursementsBannerBankAccountID(policy, bankAccountList)).toBeUndefined(); + }); + }); }); From 79b1c6b225fcc76c3d68f19e8519109851ee20f6 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 20 Oct 2026 16:48:50 +0200 Subject: [PATCH 2/6] refactor: open Wise global reimbursements intro via dynamic route --- src/ROUTES.ts | 18 ++---- src/SCREENS.ts | 3 +- .../ModalStackNavigators/index.tsx | 3 +- .../Navigation/linkingConfig/OldRoutes.ts | 1 + .../RELATIONS/SETTINGS_TO_RHP.ts | 1 - .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 - src/libs/Navigation/linkingConfig/config.ts | 8 +-- .../paramLists/AccountSettingsParamList.ts | 2 +- src/libs/Navigation/types.ts | 4 -- .../settings/Wallet/WalletPage/index.tsx | 4 +- .../WalletWiseGlobalReimbursementsBanner.tsx | 5 +- .../WiseGlobalReimbursementsIntroPage.tsx | 14 ++-- ...orkspaceWiseGlobalReimbursementsBanner.tsx | 5 +- tests/navigation/getMatchingNewRouteTest.ts | 22 +++++++ ...obalReimbursementsIntroDynamicRouteTest.ts | 64 +++++++++++++++++++ 15 files changed, 111 insertions(+), 44 deletions(-) create mode 100644 tests/navigation/wiseGlobalReimbursementsIntroDynamicRouteTest.ts diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 47554e000b7a..23babae410b0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -227,6 +227,11 @@ const DYNAMIC_ROUTES = { }), queryParams: ['bankCountry', 'bankCurrency'], }, + WISE_GLOBAL_REIMBURSEMENTS_INTRO: { + path: 'global-reimbursements/:bankAccountID', + entryScreens: [SCREENS.SETTINGS.WALLET.ROOT, SCREENS.WORKSPACE.WORKFLOWS], + getRoute: (bankAccountID: number | undefined) => `global-reimbursements/${bankAccountID}` as const, + }, BANK_ACCOUNT_VERIFY_ACCOUNT: { path: 'verify-bank-account', entryScreens: [SCREENS.REIMBURSEMENT_ACCOUNT], @@ -2636,10 +2641,6 @@ const ROUTES = { route: 'settings/wallet/:bankAccountID/share-bank-account', getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/share-bank-account` as const, }, - SETTINGS_WALLET_WISE_GLOBAL_REIMBURSEMENTS_INTRO: { - route: 'settings/wallet/:bankAccountID/global-reimbursements', - getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/global-reimbursements` as const, - }, SETTINGS_WALLET_EDIT_BANK_ACCOUNT_NICKNAME: { route: 'settings/wallet/:bankAccountID/edit-nickname', getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/edit-nickname` as const, @@ -3470,15 +3471,6 @@ const ROUTES = { return `workspaces/${policyID}/workflows/currency-conversion-fees` as const; }, }, - WORKSPACE_WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO: { - route: 'workspaces/:policyID/workflows/global-reimbursements/:bankAccountID', - getRoute: (policyID: string | undefined, bankAccountID: number | undefined) => { - if (!policyID) { - Log.warn('Invalid policyID is used to build the WORKSPACE_WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO route'); - } - return `workspaces/${policyID}/workflows/global-reimbursements/${bankAccountID}` as const; - }, - }, WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET: { route: 'workspaces/:policyID/workflows/auto-reporting-frequency/monthly-offset', getRoute: (policyID: string | undefined) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1371a4304a3d..0fa9aab933c8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -208,7 +208,7 @@ const SCREENS = { DYNAMIC_ENABLE_GLOBAL_REIMBURSEMENTS_SIGN: 'Dynamic_Settings_Wallet_Enable_Global_Reimbursements_Sign', SHARE_BANK_ACCOUNT: 'Settings_Wallet_Share_Bank_Account', EDIT_BANK_ACCOUNT_NICKNAME: 'Settings_Wallet_Edit_Bank_Account_Nickname', - WISE_GLOBAL_REIMBURSEMENTS_INTRO: 'Settings_Wallet_Wise_Global_Reimbursements_Intro', + DYNAMIC_WISE_GLOBAL_REIMBURSEMENTS_INTRO: 'Dynamic_Settings_Wallet_Wise_Global_Reimbursements_Intro', TRAVEL_CVV: 'Settings_Wallet_Travel_CVV', TRAVEL_CVV_VERIFY_ACCOUNT: 'Settings_Wallet_Travel_CVV_VerifyAccount', PERSONAL_CARD_DETAILS: 'Settings_Wallet_Personal_Card_Details', @@ -927,7 +927,6 @@ const SCREENS = { WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', WORKFLOWS_CURRENCY_CONVERSION_FEES: 'Workspace_Workflows_Currency_Conversion_Fees', - WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO: 'Workspace_Workflows_Wise_Global_Reimbursements_Intro', DESCRIPTION: 'Workspace_Overview_Description', SHARE: 'Workspace_Overview_Share', NAME: 'Workspace_Overview_Name', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 369081256599..48f418f9341b 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -498,7 +498,7 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/settings/Wallet/EnableGlobalReimbursements/EnableGlobalReimbursementsSignPage').default, [SCREENS.SETTINGS.WALLET.SHARE_BANK_ACCOUNT]: () => require('../../../../pages/settings/Wallet/ShareBankAccount/ShareBankAccount').default, [SCREENS.SETTINGS.WALLET.EDIT_BANK_ACCOUNT_NICKNAME]: () => require('../../../../pages/settings/Wallet/EditBankAccountNicknamePage').default, - [SCREENS.SETTINGS.WALLET.WISE_GLOBAL_REIMBURSEMENTS_INTRO]: () => require('../../../../pages/settings/Wallet/WiseGlobalReimbursementsIntroPage').default, + [SCREENS.SETTINGS.WALLET.DYNAMIC_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: () => require('../../../../pages/settings/Wallet/WiseGlobalReimbursementsIntroPage').default, [SCREENS.SETTINGS.WALLET.TRAVEL_CVV]: () => require('../../../../pages/settings/Wallet/TravelCVVPage/TravelCVVPage').default, [SCREENS.SETTINGS.WALLET.TRAVEL_CVV_VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Wallet/TravelCVVPage/TravelCVVVerifyAccountPage').default, [SCREENS.SETTINGS.WALLET.PERSONAL_CARD_FIX_CONNECTION]: () => require('@pages/settings/Wallet/PersonalCards/FixPersonalCardConnectionPage').default, @@ -1086,7 +1086,6 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/reconciliation/DynamicReconciliationFeedSelectorPage').default, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default, [SCREENS.WORKSPACE.WORKFLOWS_CURRENCY_CONVERSION_FEES]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsCurrencyConversionFeesPage').default, - [SCREENS.WORKSPACE.WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: () => require('../../../../pages/settings/Wallet/WiseGlobalReimbursementsIntroPage').default, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default, [SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../../pages/workspace/taxes/WorkspaceEditTaxPage').default, [SCREENS.WORKSPACE.TAX_NAME]: () => require('../../../../pages/workspace/taxes/NamePage').default, diff --git a/src/libs/Navigation/linkingConfig/OldRoutes.ts b/src/libs/Navigation/linkingConfig/OldRoutes.ts index 3c3912974e98..3eda85c9d008 100644 --- a/src/libs/Navigation/linkingConfig/OldRoutes.ts +++ b/src/libs/Navigation/linkingConfig/OldRoutes.ts @@ -143,6 +143,7 @@ const oldRoutes: Record = { '/workspaces/*/expensify-card/*': '/workspaces/$1/expensify-card/card-details/$2', '/workspace/confirmation/currency': '/workspaces/workspace/confirmation/currency', '/workspace/confirmation': '/workspaces/workspace/confirmation', + '/settings/wallet/*/global-reimbursements': '/settings/wallet/global-reimbursements/$1', '/settings/wallet/enable-payments/plaid*': '/settings/wallet/enable-payments/add-bank-account/plaid$1', '/settings/wallet/enable-payments/bank-account-confirmation*': '/settings/wallet/enable-payments/add-bank-account/confirmation$1', '/settings/wallet/enable-payments/legal-name*': '/settings/wallet/enable-payments/personal-info/legal-name$1', diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 551cf527dfd4..cd258c3841e1 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -52,7 +52,6 @@ const SETTINGS_TO_RHP: Partial['config'] = { path: ROUTES.SETTINGS_WALLET_SHARE_BANK_ACCOUNT.route, exact: true, }, - [SCREENS.SETTINGS.WALLET.WISE_GLOBAL_REIMBURSEMENTS_INTRO]: { - path: ROUTES.SETTINGS_WALLET_WISE_GLOBAL_REIMBURSEMENTS_INTRO.route, - exact: true, - }, + [SCREENS.SETTINGS.WALLET.DYNAMIC_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.path, [SCREENS.SETTINGS.WALLET.EDIT_BANK_ACCOUNT_NICKNAME]: { path: ROUTES.SETTINGS_WALLET_EDIT_BANK_ACCOUNT_NICKNAME.route, exact: true, @@ -946,9 +943,6 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.WORKFLOWS_CURRENCY_CONVERSION_FEES]: { path: ROUTES.WORKSPACE_WORKFLOWS_CURRENCY_CONVERSION_FEES.route, }, - [SCREENS.WORKSPACE.WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: { - path: ROUTES.WORKSPACE_WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO.route, - }, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: { path: ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET.route, }, diff --git a/src/libs/Navigation/paramLists/AccountSettingsParamList.ts b/src/libs/Navigation/paramLists/AccountSettingsParamList.ts index 9be30b8905ee..c32761de283a 100644 --- a/src/libs/Navigation/paramLists/AccountSettingsParamList.ts +++ b/src/libs/Navigation/paramLists/AccountSettingsParamList.ts @@ -184,7 +184,7 @@ type AccountSettingsParamList = { [SCREENS.SETTINGS.WALLET.SHARE_BANK_ACCOUNT]: { bankAccountID: string; }; - [SCREENS.SETTINGS.WALLET.WISE_GLOBAL_REIMBURSEMENTS_INTRO]: { + [SCREENS.SETTINGS.WALLET.DYNAMIC_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: { bankAccountID: string; }; [SCREENS.SETTINGS.WALLET.EDIT_BANK_ACCOUNT_NICKNAME]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9531cf7bf48c..3774fd074252 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1149,10 +1149,6 @@ type WorkspaceSplitNavigatorParamList = { [SCREENS.WORKSPACE.WORKFLOWS_CURRENCY_CONVERSION_FEES]: { policyID: string; }; - [SCREENS.WORKSPACE.WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO]: { - policyID: string; - bankAccountID: string; - }; [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: { policyID: string; }; diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 57a5682511a8..6118b4e9342a 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -607,7 +607,9 @@ function WalletPage() { return; } if (isWiseGlobalReimbursementsEnabled) { - closeModal(() => Navigation.navigate(ROUTES.SETTINGS_WALLET_WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(paymentMethod.selectedPaymentMethod.bankAccountID))); + closeModal(() => + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(paymentMethod.selectedPaymentMethod.bankAccountID))), + ); return; } closeModal(() => diff --git a/src/pages/settings/Wallet/WalletWiseGlobalReimbursementsBanner.tsx b/src/pages/settings/Wallet/WalletWiseGlobalReimbursementsBanner.tsx index 0ef3e07a55c2..3989069a6774 100644 --- a/src/pages/settings/Wallet/WalletWiseGlobalReimbursementsBanner.tsx +++ b/src/pages/settings/Wallet/WalletWiseGlobalReimbursementsBanner.tsx @@ -7,11 +7,12 @@ import useShouldSuppressPromotionalUI from '@hooks/useShouldSuppressPromotionalU import {dismissProductTraining} from '@libs/actions/Welcome'; import {getWiseGlobalReimbursementsBannerBankAccountID} from '@libs/BankAccountUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {DismissedProductTraining} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -38,7 +39,7 @@ function WalletWiseGlobalReimbursementsBanner() { return ( Navigation.navigate(ROUTES.SETTINGS_WALLET_WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(bankAccountID))} + onGetStartedPress={() => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(bankAccountID)))} onDismissPress={() => dismissProductTraining(CONST.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER, true)} sentryLabels={CONST.SENTRY_LABEL.WISE_GLOBAL_REIMBURSEMENTS_WALLET_BANNER} /> diff --git a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx index befed818e420..68f77d63c0a8 100644 --- a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx +++ b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx @@ -4,6 +4,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Header from '@components/Header'; import ScreenWrapper from '@components/ScreenWrapper'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -16,26 +17,23 @@ import {isBankAccountEligibleForGlobalReimbursements} from '@libs/BankAccountUti import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import Navigation from '@navigation/Navigation'; -import type {SettingsNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@navigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import React, {useEffect} from 'react'; -type WiseGlobalReimbursementsIntroPageProps = - | PlatformStackScreenProps - | PlatformStackScreenProps; +type WiseGlobalReimbursementsIntroPageProps = PlatformStackScreenProps; function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntroPageProps) { const bankAccountID = Number(route.params.bankAccountID); - // Opened from Workspace > Workflows > Payments when the route carries a workspace, from Wallet otherwise - const policyID = 'policyID' in route.params ? route.params.policyID : undefined; + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.path); const [bankAccountList, bankAccountListMetadata] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [isLoadingPaymentMethods = true] = useOnyx(ONYXKEYS.IS_LOADING_PAYMENT_METHODS); const bankAccount = bankAccountList?.[bankAccountID]; @@ -47,7 +45,7 @@ function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntr const StyleUtils = useStyleUtils(); const illustrations = useMemoizedLazyIllustrations(['EarthWithCoins']); - const goBackToEntryPoint = () => Navigation.goBack(policyID ? ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID, CONST.TAB.WORKFLOWS.PAYMENTS) : ROUTES.SETTINGS_WALLET); + const goBackToEntryPoint = () => Navigation.goBack(backPath); // Close the page instead of showing "not found" when the user lacks the beta. Wait for betas to load first useEffect(() => { diff --git a/src/pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner.tsx b/src/pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner.tsx index 0764f36a41bb..2bce00f4dbbd 100644 --- a/src/pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner.tsx +++ b/src/pages/workspace/workflows/WorkspaceWiseGlobalReimbursementsBanner.tsx @@ -9,12 +9,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {dismissProductTraining} from '@libs/actions/Welcome'; import {getWorkspaceWiseGlobalReimbursementsBannerBankAccountID} from '@libs/BankAccountUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {isPolicyAdmin} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {DismissedProductTraining} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -48,7 +49,7 @@ function WorkspaceWiseGlobalReimbursementsBanner({policyID}: WorkspaceWiseGlobal return ( Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(policyID, bankAccountID))} + onGetStartedPress={() => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(bankAccountID)))} onDismissPress={() => dismissProductTraining(CONST.WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_BANNER, true)} sentryLabels={CONST.SENTRY_LABEL.WISE_GLOBAL_REIMBURSEMENTS_WORKSPACE_BANNER} /> diff --git a/tests/navigation/getMatchingNewRouteTest.ts b/tests/navigation/getMatchingNewRouteTest.ts index 612ff85c0a73..dbbcd3608c25 100644 --- a/tests/navigation/getMatchingNewRouteTest.ts +++ b/tests/navigation/getMatchingNewRouteTest.ts @@ -374,6 +374,28 @@ describe('getBestMatchingPath', () => { expect(getMatchingNewRoute('/home-page?backTo=r/123')).toBe('/home?backTo=r/123'); }); + it('redirects the old Wallet global reimbursements intro path to its dynamic route', () => { + // Given a link to the intro page saved before it became a dynamic route, keyed by bank account ID + const oldRoute = '/settings/wallet/123/global-reimbursements'; + + // When the old link is resolved + const newRoute = getMatchingNewRoute(oldRoute); + + // Then it lands on the dynamic suffix appended to Wallet, so back navigation still returns to Wallet + expect(newRoute).toBe('/settings/wallet/global-reimbursements/123'); + }); + + it('does not redirect the dynamic global reimbursements intro route', () => { + // Given the new dynamic intro path, whose segment order differs from the old pattern + const newRoute = '/settings/wallet/global-reimbursements/123'; + + // When it is resolved against the old routes + const matchedRoute = getMatchingNewRoute(newRoute); + + // Then nothing matches, so it is not rewritten in a loop + expect(matchedRoute).toBe(undefined); + }); + describe('redirects old enable-payments substeps to their new routes', () => { const enablePaymentsCases = [ {oldRoute: '/settings/wallet/enable-payments/plaid', expectedRoute: '/settings/wallet/enable-payments/add-bank-account/plaid'}, diff --git a/tests/navigation/wiseGlobalReimbursementsIntroDynamicRouteTest.ts b/tests/navigation/wiseGlobalReimbursementsIntroDynamicRouteTest.ts new file mode 100644 index 000000000000..fbf1ab8cfc6a --- /dev/null +++ b/tests/navigation/wiseGlobalReimbursementsIntroDynamicRouteTest.ts @@ -0,0 +1,64 @@ +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; +import findAllMatchingDynamicSuffixes from '@libs/Navigation/helpers/dynamicRoutesUtils/findAllMatchingDynamicSuffixes'; +import getPathWithoutDynamicSuffix from '@libs/Navigation/helpers/dynamicRoutesUtils/getPathWithoutDynamicSuffix'; +import getStateFromPath from '@libs/Navigation/helpers/getStateFromPath'; + +import CONST from '@src/CONST'; +import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; + +import type {NavigationState, PartialState} from '@react-navigation/native'; + +const SUFFIX = DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.path; +const BANK_ACCOUNT_ID = 9053192; +const POLICY_ID = 'ABCDEF1234567890'; + +type FocusedRoute = {name?: string; params?: unknown}; + +function getFocusedRoute(state: PartialState): FocusedRoute { + let current: PartialState | undefined = state; + let route: FocusedRoute = {}; + + while (current?.routes) { + const nextRoute = current.routes.at(current.index ?? current.routes.length - 1); + route = {name: nextRoute?.name, params: nextRoute?.params}; + current = nextRoute?.state; + } + + return route; +} + +const ENTRY_POINTS = [ + ['Wallet', ROUTES.SETTINGS_WALLET], + ['Workspace Workflows > Payments', ROUTES.WORKSPACE_WORKFLOWS.getRoute(POLICY_ID, CONST.TAB.WORKFLOWS.PAYMENTS)], +] as const; + +describe('Wise global reimbursements intro dynamic route', () => { + it.each(ENTRY_POINTS)('opens the intro page on top of %s', (_label, basePath) => { + // Given the intro suffix appended to an entry point the banner or bank account menu lives on + const route = createDynamicRoute(DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(BANK_ACCOUNT_ID), basePath); + + // When the URL is parsed into navigation state + const focusedRoute = getFocusedRoute(getStateFromPath(route)); + + // Then the single dynamic intro screen is focused with the bank account from the suffix + expect(focusedRoute.name).toBe(SCREENS.SETTINGS.WALLET.DYNAMIC_WISE_GLOBAL_REIMBURSEMENTS_INTRO); + expect(focusedRoute.params).toMatchObject({bankAccountID: String(BANK_ACCOUNT_ID)}); + }); + + it.each(ENTRY_POINTS)('goes back to %s once the suffix is stripped', (_label, basePath) => { + // Given the intro page opened on top of an entry point + const route = createDynamicRoute(DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(BANK_ACCOUNT_ID), basePath); + + // When computing the back path from the current URL + const match = findAllMatchingDynamicSuffixes(route).find((m) => m.pattern === SUFFIX); + expect(match).toBeDefined(); + if (!match) { + return; + } + const backPath = getPathWithoutDynamicSuffix(match.pathUsedForMatching, match.actualSuffix, match.pattern); + + // Then going back lands on the page the user came from, including the selected Workflows tab + expect(backPath).toBe(basePath); + }); +}); From 656013864aa10d0f7e982f7abb09d4c4cc7abb2b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 21 Oct 2026 08:54:58 +0200 Subject: [PATCH 3/6] docs: replace arrow with plain words in Wise banner bank account comment --- src/libs/BankAccountUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index ad09a2c5c952..adacf97de2e4 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -167,7 +167,7 @@ function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEnt return accountData; } - // `created` is a DB timestamp, so string compare works. Same second → higher ID wins + // `created` is a DB timestamp, so string comparison orders accounts chronologically. When two accounts share the same second, the higher bank account ID wins. const created = accountData.created ?? ''; const latestCreated = latest.created ?? ''; const isNewer = created > latestCreated || (created === latestCreated && accountData.bankAccountID > (latest.bankAccountID ?? CONST.DEFAULT_NUMBER_ID)); From ad4919775e21f3f1cfb425d95040aeb6d99508ce Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 21 Oct 2026 08:58:55 +0200 Subject: [PATCH 4/6] fix: fall back to bankCurrency when checking Wise global reimbursements eligibility --- src/libs/BankAccountUtils.ts | 15 +++++++++------ .../Wallet/WiseGlobalReimbursementsIntroPage.tsx | 2 +- tests/unit/BankAccountUtilsTest.ts | 7 +++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts index adacf97de2e4..27594a3b2bb8 100644 --- a/src/libs/BankAccountUtils.ts +++ b/src/libs/BankAccountUtils.ts @@ -138,12 +138,15 @@ 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. + * Pass the record's top-level `bankCurrency` when available, since the nested `additionalData.currency` is optional. */ -function isBankAccountEligibleForGlobalReimbursements(accountData: AccountData | undefined): boolean { +function isBankAccountEligibleForGlobalReimbursements(accountData: AccountData | undefined, bankCurrency?: string): boolean { return ( - accountData?.additionalData?.currency === CONST.CURRENCY.USD && - accountData.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && - !accountData.additionalData.verifications?.corpay && + // `||` not `??`: bankCurrency can be an empty string, which should fall through to additionalData. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (bankCurrency || accountData?.additionalData?.currency) === CONST.CURRENCY.USD && + accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && + !accountData.additionalData?.verifications?.corpay && accountData.state === CONST.BANK_ACCOUNT.STATE.OPEN ); } @@ -159,7 +162,7 @@ function getWiseGlobalReimbursementsBannerBankAccountID(bankAccountList: OnyxEnt } const accountData = bankAccount.accountData; - if (!accountData?.bankAccountID || !isBankAccountEligibleForGlobalReimbursements(accountData)) { + if (!accountData?.bankAccountID || !isBankAccountEligibleForGlobalReimbursements(accountData, bankAccount.bankCurrency)) { return latest; } @@ -194,7 +197,7 @@ function getWorkspaceWiseGlobalReimbursementsBannerBankAccountID(policy: OnyxEnt // Admins without a copy of the account can't see its currency, type or verifications, so the banner stays hidden for them const bankAccount = bankAccountList?.[bankAccountID]; - if (bankAccount?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isBankAccountEligibleForGlobalReimbursements(bankAccount?.accountData)) { + if (bankAccount?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isBankAccountEligibleForGlobalReimbursements(bankAccount?.accountData, bankAccount?.bankCurrency)) { return undefined; } diff --git a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx index 68f77d63c0a8..89bd5e4425bd 100644 --- a/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx +++ b/src/pages/settings/Wallet/WiseGlobalReimbursementsIntroPage.tsx @@ -65,7 +65,7 @@ function WiseGlobalReimbursementsIntroPage({route}: WiseGlobalReimbursementsIntr return (
diff --git a/tests/unit/BankAccountUtilsTest.ts b/tests/unit/BankAccountUtilsTest.ts index 26d4524e5287..0ae2198202af 100644 --- a/tests/unit/BankAccountUtilsTest.ts +++ b/tests/unit/BankAccountUtilsTest.ts @@ -267,6 +267,13 @@ describe('BankAccountUtils', () => { expect(isBankAccountEligibleForGlobalReimbursements({...eligibleAccountData, additionalData: {}})).toBe(false); }); + it('returns true when only the top-level bank currency is USD', () => { + // Given a business account whose nested currency is missing but whose record carries bankCurrency USD + // When checking eligibility with that bankCurrency + // Then it is eligible, because bankCurrency is the required field and the nested currency is optional + expect(isBankAccountEligibleForGlobalReimbursements({...eligibleAccountData, additionalData: {}}, CONST.CURRENCY.USD)).toBe(true); + }); + 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 From af3023addf9e33e5e310960749d0e60f66d78130 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 21 Oct 2026 09:38:27 +0200 Subject: [PATCH 5/6] fix: move Enable Global Reimbursements to second item in Wallet bank account menu --- .../settings/Wallet/WalletPage/index.tsx | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index 6118b4e9342a..6d4cee92976e 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -538,6 +538,34 @@ function WalletPage() { }, ] : []), + ...(shouldShowEnableGlobalReimbursementsButton + ? [ + { + text: translate('common.enableGlobalReimbursements'), + icon: icons.Globe, + onSelected: () => { + if (isAccountLocked) { + closeModal(() => showLockedAccountModal()); + return; + } + if (isWiseGlobalReimbursementsEnabled) { + closeModal(() => + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(paymentMethod.selectedPaymentMethod.bankAccountID))), + ); + return; + } + closeModal(() => + Navigation.navigate( + ROUTES.SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS_BUSINESS.getRoute( + paymentMethod.selectedPaymentMethod.bankAccountID, + CONST.ENABLE_GLOBAL_REIMBURSEMENTS.PAGE_NAME.BUSINESS_INFO.REGISTRATION_NUMBER, + ), + ), + ); + }, + }, + ] + : []), ...(shouldShowEditNicknameButton ? [ { @@ -596,34 +624,6 @@ function WalletPage() { }); }, }, - ...(shouldShowEnableGlobalReimbursementsButton - ? [ - { - text: translate('common.enableGlobalReimbursements'), - icon: icons.Globe, - onSelected: () => { - if (isAccountLocked) { - closeModal(() => showLockedAccountModal()); - return; - } - if (isWiseGlobalReimbursementsEnabled) { - closeModal(() => - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.WISE_GLOBAL_REIMBURSEMENTS_INTRO.getRoute(paymentMethod.selectedPaymentMethod.bankAccountID))), - ); - return; - } - closeModal(() => - Navigation.navigate( - ROUTES.SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS_BUSINESS.getRoute( - paymentMethod.selectedPaymentMethod.bankAccountID, - CONST.ENABLE_GLOBAL_REIMBURSEMENTS.PAGE_NAME.BUSINESS_INFO.REGISTRATION_NUMBER, - ), - ), - ); - }, - }, - ] - : []), ], [ shouldUseNarrowLayout, From b8c2b6a252a616656add332129007764b2f1e910 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 21 Oct 2026 13:12:29 +0200 Subject: [PATCH 6/6] feat: add close tooltip to Wise global reimbursements banner --- .../WiseGlobalReimbursementsBanner.tsx | 1 + .../BillingBanner/BillingBanner.tsx | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/components/WiseGlobalReimbursementsBanner.tsx b/src/components/WiseGlobalReimbursementsBanner.tsx index 2523b81810a9..10410714aeaf 100644 --- a/src/components/WiseGlobalReimbursementsBanner.tsx +++ b/src/components/WiseGlobalReimbursementsBanner.tsx @@ -43,6 +43,7 @@ function WiseGlobalReimbursementsBanner({onGetStartedPress, onDismissPress, sent rightIcon={expensifyIcons.Close} onRightIconPress={onDismissPress} rightIconAccessibilityLabel={translate('common.dismiss')} + rightIconTooltipText={translate('common.close')} rightIconSentryLabel={sentryLabels.DISMISS} rightComponent={ diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx index 92e5477df3be..e8769686c6c6 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx @@ -1,6 +1,7 @@ import Icon from '@components/Icon'; import {PressableWithoutFeedback} from '@components/Pressable'; import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -39,6 +40,9 @@ type BillingBannerProps = { onRightIconPress?: () => void; rightIconAccessibilityLabel?: string; + /** Text of the tooltip shown on hover of the right icon button. No tooltip is shown when omitted. */ + rightIconTooltipText?: string; + /** Sentry label for the right icon button. Defaults to `CONST.SENTRY_LABEL.BILLING_BANNER.RIGHT_ICON`. */ rightIconSentryLabel?: string; @@ -57,6 +61,7 @@ function BillingBanner({ rightIcon, onRightIconPress, rightIconAccessibilityLabel, + rightIconTooltipText, rightIconSentryLabel, rightComponent, }: BillingBannerProps) { @@ -67,20 +72,25 @@ function BillingBanner({ const rightIconComponent = useMemo(() => { if (rightIcon) { - return onRightIconPress && rightIconAccessibilityLabel ? ( - - - - ) : ( + if (onRightIconPress && rightIconAccessibilityLabel) { + const rightIconButton = ( + + + + ); + return rightIconTooltipText ? {rightIconButton} : rightIconButton; + } + + return (