From 64c9cf6f9df38687b0ad72d06995aaf0ffb0f74d Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 2 Apr 2026 17:12:08 +0200 Subject: [PATCH 01/23] Add unit tests for UnlockBankAccount time-sensitive widget --- .../UnlockBankAccountTest.tsx | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx diff --git a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx new file mode 100644 index 000000000000..14ce037721c4 --- /dev/null +++ b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx @@ -0,0 +1,240 @@ +import {fireEvent, render, screen} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import Navigation from '@libs/Navigation/Navigation'; +import OnyxListItemProvider from '@src/components/OnyxListItemProvider'; +import CONST from '@src/CONST'; +import * as BankAccountsActions from '@src/libs/actions/BankAccounts'; +import ONYXKEYS from '@src/ONYXKEYS'; +import TimeSensitiveSection from '@src/pages/home/TimeSensitiveSection'; +import waitForBatchedUpdates from '../../../../utils/waitForBatchedUpdates'; + +jest.mock('@libs/Navigation/Navigation'); + +jest.mock('@hooks/useLocalize', () => jest.fn(() => ({translate: jest.fn((key: string) => key)}))); + +jest.mock('@hooks/useLazyAsset', () => ({ + useMemoizedLazyExpensifyIcons: jest.fn(() => ({ + EnvelopeOpenStar: () => null, + Bank: () => null, + })), +})); + +jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard', () => + jest.fn(() => ({ + shouldShowAddPaymentCard: false, + })), +); + +jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards', () => + jest.fn(() => ({ + shouldShowAddShippingAddress: false, + shouldShowActivateCard: false, + shouldShowReviewCardFraud: false, + cardsNeedingShippingAddress: [], + cardsNeedingActivation: [], + cardsWithFraud: [], + })), +); + +jest.mock('@hooks/useCardFeedErrors', () => + jest.fn(() => ({ + cardsWithBrokenFeedConnection: {}, + personalCardsWithBrokenConnection: {}, + })), +); + +jest.mock('@hooks/useCurrentUserPersonalDetails', () => jest.fn(() => ({login: 'admin@example.com'}))); + +jest.mock('@hooks/useResponsiveLayout', () => jest.fn(() => ({shouldUseNarrowLayout: false}))); + +jest.mock('@src/libs/actions/BankAccounts', () => ({ + pressLockedBankAccount: jest.fn(), +})); + +const ADMIN_ACCOUNT_ID = 12345; +const LOCKED_BANK_ACCOUNT_ID = 99; +const POLICY_ID = 'policy_1'; +const POLICY_NAME = 'My Workspace'; +const CONCIERGE_REPORT_ID = 'concierge_report_1'; + +const renderTimeSensitiveSection = () => + render( + + + , + ); + +describe('TimeSensitiveSection - UnlockBankAccount', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + it('renders UnlockBankAccount widget when a workspace policy has achAccount.state === LOCKED', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, { + id: POLICY_ID, + name: POLICY_NAME, + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + isPolicyExpenseChatEnabled: true, + achAccount: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + accountNumber: 'XXXXXXXX1234', + routingNumber: '123456789', + addressName: 'Test Bank', + bankName: 'Test Bank', + reimburser: 'admin@example.com', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + expect(screen.getByText('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle')).toBeTruthy(); + }); + + it('renders UnlockBankAccount widget when BANK_ACCOUNT_LIST has an entry with accountData.state === LOCKED', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, { + [`bankAccount-${LOCKED_BANK_ACCOUNT_ID}`]: { + bankCurrency: 'USD', + bankCountry: 'US', + accountData: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }, + }); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + expect(screen.getByText('homePage.timeSensitiveSection.unlockBankAccount.personalTitle')).toBeTruthy(); + }); + + it('does NOT render UnlockBankAccount when bank account state is OPEN', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, { + [`bankAccount-${LOCKED_BANK_ACCOUNT_ID}`]: { + bankCurrency: 'USD', + bankCountry: 'US', + accountData: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + }, + }, + }); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + expect(screen.queryByText('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle')).toBeNull(); + expect(screen.queryByText('homePage.timeSensitiveSection.unlockBankAccount.personalTitle')).toBeNull(); + }); + + it('does NOT render UnlockBankAccount for non-admin users (workspace VBA case)', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: 'member@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, { + id: POLICY_ID, + name: POLICY_NAME, + role: CONST.POLICY.ROLE.USER, + type: CONST.POLICY.TYPE.TEAM, + isPolicyExpenseChatEnabled: true, + achAccount: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + accountNumber: 'XXXXXXXX1234', + routingNumber: '123456789', + addressName: 'Test Bank', + bankName: 'Test Bank', + reimburser: 'admin@example.com', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + expect(screen.queryByText('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle')).toBeNull(); + }); + + it('renders multiple UnlockBankAccount widgets when multiple locked accounts exist', async () => { + const SECOND_POLICY_ID = 'policy_2'; + const SECOND_LOCKED_BANK_ACCOUNT_ID = 100; + + await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, { + id: POLICY_ID, + name: 'Workspace One', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + isPolicyExpenseChatEnabled: true, + achAccount: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + accountNumber: 'XXXXXXXX1234', + routingNumber: '123456789', + addressName: 'Test Bank', + bankName: 'Test Bank', + reimburser: 'admin@example.com', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${SECOND_POLICY_ID}`, { + id: SECOND_POLICY_ID, + name: 'Workspace Two', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + isPolicyExpenseChatEnabled: true, + achAccount: { + bankAccountID: SECOND_LOCKED_BANK_ACCOUNT_ID, + accountNumber: 'XXXXXXXX5678', + routingNumber: '987654321', + addressName: 'Another Bank', + bankName: 'Another Bank', + reimburser: 'admin@example.com', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + const widgets = screen.getAllByText('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle'); + expect(widgets).toHaveLength(2); + }); + + it('calls pressLockedBankAccount and navigates to Concierge when CTA is pressed', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.set(ONYXKEYS.CONCIERGE_REPORT_ID, CONCIERGE_REPORT_ID); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, { + id: POLICY_ID, + name: POLICY_NAME, + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + isPolicyExpenseChatEnabled: true, + achAccount: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + accountNumber: 'XXXXXXXX1234', + routingNumber: '123456789', + addressName: 'Test Bank', + bankName: 'Test Bank', + reimburser: 'admin@example.com', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + const cta = screen.getByText('homePage.timeSensitiveSection.ctaFix'); + fireEvent.press(cta); + + expect(BankAccountsActions.pressLockedBankAccount).toHaveBeenCalledWith(LOCKED_BANK_ACCOUNT_ID, expect.any(Function), CONCIERGE_REPORT_ID); + expect(Navigation.navigate).toHaveBeenCalled(); + }); +}); From 810078ef114b8499c370caae575f32d6e5e0edf0 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 3 Apr 2026 12:27:30 +0200 Subject: [PATCH 02/23] Add translations --- src/languages/en.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 936ae26c37fe..d2898b42cecf 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1016,6 +1016,12 @@ const translations = { title: "We couldn't bill your card on file", subtitle: 'Subscription', }, + unlockBankAccount: { + workspaceTitle: 'Your business bank account has been locked', + personalTitle: 'Your bank account has been locked', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, + personalSubtitle: 'Wallet', + }, }, freeTrialSection: { title: ({days}: {days: number}) => `Free trial: ${days} ${days === 1 ? 'day' : 'days'} left!`, From a0629f012b3cd176726712e1a7ac7a34b7584b95 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 3 Apr 2026 12:32:46 +0200 Subject: [PATCH 03/23] Add UnlockBankAccount widget to TimeSensitiveSection --- src/pages/home/TimeSensitiveSection/index.tsx | 50 +++++++++++++++--- .../items/UnlockBankAccount.tsx | 52 +++++++++++++++++++ 2 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index 7c9225e8fbf6..20bc6951ae1b 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -14,6 +14,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {hasSynchronizationErrorMessage, isConnectionInProgress} from '@libs/actions/connections'; import {isCurrentUserValidated} from '@libs/UserUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy} from '@src/types/onyx'; import type {ConnectionName, PolicyConnectionName} from '@src/types/onyx/Policy'; @@ -28,6 +29,7 @@ import FixCompanyCardConnection from './items/FixCompanyCardConnection'; import FixFailedBilling from './items/FixFailedBilling'; import FixPersonalCardConnection from './items/FixPersonalCardConnection'; import ReviewCardFraud from './items/ReviewCardFraud'; +import UnlockBankAccount from './items/UnlockBankAccount'; import ValidateAccount from './items/ValidateAccount'; type BrokenAccountingConnection = { @@ -57,6 +59,14 @@ type BrokenPersonalCardConnection = { cardID: string; }; +type LockedBankAccount = { + /** The ID of the locked bank account */ + bankAccountID: number; + + /** The policy name — undefined means personal account */ + policyName?: string; +}; + function TimeSensitiveSection() { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -80,6 +90,7 @@ function TimeSensitiveSection() { }); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); // Get card feed errors for company card connections (Release 4) const cardFeedErrors = useCardFeedErrors(); @@ -143,6 +154,22 @@ function TimeSensitiveSection() { } } + // Find locked bank accounts — workspace VBAs (admin policies with locked achAccount) and personal accounts + const lockedBankAccounts: LockedBankAccount[] = []; + const workspaceLockedBankAccountIDs = new Set(); + for (const policy of adminPolicies ?? []) { + if (policy.achAccount?.state === CONST.BANK_ACCOUNT.STATE.LOCKED && policy.achAccount.bankAccountID) { + lockedBankAccounts.push({bankAccountID: policy.achAccount.bankAccountID, policyName: policy.name}); + workspaceLockedBankAccountIDs.add(policy.achAccount.bankAccountID); + } + } + for (const account of Object.values(bankAccountList ?? {})) { + const {bankAccountID, state} = account.accountData ?? {}; + if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !workspaceLockedBankAccountIDs.has(bankAccountID)) { + lockedBankAccounts.push({bankAccountID}); + } + } + const hasBrokenCompanyCards = brokenCompanyCardConnections.length > 0; const hasBrokenPersonalCards = brokenPersonalCardConnections.length > 0; const hasBrokenAccountingConnections = brokenAccountingConnections.length > 0; @@ -153,6 +180,7 @@ function TimeSensitiveSection() { // If a widget has additional conditions in the render (e.g. && !!discountInfo), those // must be reflected here to avoid showing an empty "Time sensitive" section. const hasAnyTimeSensitiveContent = + lockedBankAccounts.length > 0 || shouldShowValidateAccount || shouldShowFixFailedBilling || shouldShowReviewCardFraud || @@ -174,9 +202,10 @@ function TimeSensitiveSection() { // 4. Add payment card (trial ended, no payment card) // 5. Broken bank connections (company cards) // 6. Broken bank connections (personal cards) - // 7. Broken accounting connections - // 8. Expensify card shipping - // 9. Expensify card activation + // 7. Locked bank accounts (workspace VBAs and personal) + // 8. Broken accounting connections + // 9. Expensify card shipping + // 10. Expensify card activation return ( @@ -231,7 +260,16 @@ function TimeSensitiveSection() { ); })} - {/* Priority 7: Broken accounting connections */} + {/* Priority 7: Locked bank accounts */} + {lockedBankAccounts.map((account) => ( + + ))} + + {/* Priority 8: Broken accounting connections */} {brokenAccountingConnections.map((connection) => ( ))} - {/* Priority 8: Expensify card shipping */} + {/* Priority 9: Expensify card shipping */} {shouldShowAddShippingAddress && cardsNeedingShippingAddress.map((card) => ( ))} - {/* Priority 9: Expensify card activation */} + {/* Priority 10: Expensify card activation */} {shouldShowActivateCard && cardsNeedingActivation.map((card) => ( { + pressLockedBankAccount(bankAccountID, translate, conciergeReportID); + if (conciergeReportID) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeReportID)); + } + }; + + return ( + + ); +} + +export default UnlockBankAccount; From cdf5c6e3f26b6955ec963b4b9a7ee0e3bb9c2901 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 21 Apr 2026 13:37:47 +0200 Subject: [PATCH 04/23] Add BankLock icon asset and export --- assets/images/bank-lock.svg | 5 +++++ src/components/Icon/chunks/expensify-icons.chunk.ts | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 assets/images/bank-lock.svg diff --git a/assets/images/bank-lock.svg b/assets/images/bank-lock.svg new file mode 100644 index 000000000000..4a0b4f19dc4a --- /dev/null +++ b/assets/images/bank-lock.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts index d58a693f77ea..ed398e1e340d 100644 --- a/src/components/Icon/chunks/expensify-icons.chunk.ts +++ b/src/components/Icon/chunks/expensify-icons.chunk.ts @@ -19,6 +19,7 @@ import FallbackWorkspaceAvatar from '@assets/images/avatars/fallback-workspace-a import NotificationsAvatar from '@assets/images/avatars/notifications-avatar.svg'; import ActiveRoomAvatar from '@assets/images/avatars/room.svg'; import BackArrow from '@assets/images/back-left.svg'; +import BankLock from '@assets/images/bank-lock.svg'; import Bank from '@assets/images/bank.svg'; import Basket from '@assets/images/basket.svg'; import BedCircleSlash from '@assets/images/bed-circle-slash.svg'; @@ -276,6 +277,7 @@ const Expensicons = { AttachmentNotFound, BackArrow, Bank, + BankLock, Basket, CircularArrowBackwards, Bill, From cca8e5118900a180bfdfb9468c33b5f9edeab545 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 21 Apr 2026 13:53:29 +0200 Subject: [PATCH 05/23] Update UnlockBankAccount concierge chat handling --- .../items/UnlockBankAccount.tsx | 17 ++++++++++------- .../UnlockBankAccountTest.tsx | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx b/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx index 241439e3dee5..dd89613714b8 100644 --- a/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx +++ b/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx @@ -1,13 +1,14 @@ +import {hasSeenTourSelector} from '@selectors/Onboarding'; import React from 'react'; import BaseWidgetItem from '@components/BaseWidgetItem'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {pressLockedBankAccount} from '@libs/actions/BankAccounts'; -import Navigation from '@libs/Navigation/Navigation'; +import {navigateToConciergeChat} from '@libs/actions/Report'; import colors from '@styles/theme/colors'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; type UnlockBankAccountProps = { /** The ID of the locked bank account */ @@ -19,8 +20,12 @@ type UnlockBankAccountProps = { function UnlockBankAccount({bankAccountID, policyName}: UnlockBankAccountProps) { const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Bank']); + const icons = useMemoizedLazyExpensifyIcons(['BankLock']); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [betas] = useOnyx(ONYXKEYS.BETAS); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const title = policyName ? translate('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle') : translate('homePage.timeSensitiveSection.unlockBankAccount.personalTitle'); @@ -30,14 +35,12 @@ function UnlockBankAccount({bankAccountID, policyName}: UnlockBankAccountProps) const handleCtaPress = () => { pressLockedBankAccount(bankAccountID, translate, conciergeReportID); - if (conciergeReportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeReportID)); - } + navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas); }; return ( jest.fn(() => ({translate: jest.fn((key: string) => key)}))); jest.mock('@hooks/useLazyAsset', () => ({ useMemoizedLazyExpensifyIcons: jest.fn(() => ({ EnvelopeOpenStar: () => null, - Bank: () => null, + BankLock: () => null, })), })); @@ -43,14 +41,18 @@ jest.mock('@hooks/useCardFeedErrors', () => })), ); -jest.mock('@hooks/useCurrentUserPersonalDetails', () => jest.fn(() => ({login: 'admin@example.com'}))); +jest.mock('@hooks/useCurrentUserPersonalDetails', () => jest.fn(() => ({login: 'admin@example.com', accountID: 12345}))); jest.mock('@hooks/useResponsiveLayout', () => jest.fn(() => ({shouldUseNarrowLayout: false}))); -jest.mock('@src/libs/actions/BankAccounts', () => ({ +jest.mock('@libs/actions/BankAccounts', () => ({ pressLockedBankAccount: jest.fn(), })); +jest.mock('@libs/actions/Report', () => ({ + navigateToConciergeChat: jest.fn(), +})); + const ADMIN_ACCOUNT_ID = 12345; const LOCKED_BANK_ACCOUNT_ID = 99; const POLICY_ID = 'policy_1'; @@ -235,6 +237,6 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { fireEvent.press(cta); expect(BankAccountsActions.pressLockedBankAccount).toHaveBeenCalledWith(LOCKED_BANK_ACCOUNT_ID, expect.any(Function), CONCIERGE_REPORT_ID); - expect(Navigation.navigate).toHaveBeenCalled(); + expect(ReportActions.navigateToConciergeChat).toHaveBeenCalledWith(CONCIERGE_REPORT_ID, undefined, ADMIN_ACCOUNT_ID, false, undefined); }); }); From 55da97e328a1099a67ba21c0f62d29255c53aeb0 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 21 Apr 2026 14:56:21 +0200 Subject: [PATCH 06/23] Extract locked bank account detection into a dedicated time-sensitive hook --- .../useTimeSensitiveLockedBankAccount.ts | 35 +++++++++++++++++++ src/pages/home/TimeSensitiveSection/index.tsx | 27 ++------------ 2 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts new file mode 100644 index 000000000000..fa29597f02e4 --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts @@ -0,0 +1,35 @@ +import CONST from '@src/CONST'; +import type {BankAccountList, Policy} from '@src/types/onyx'; + +type LockedBankAccount = { + /** The ID of the locked bank account */ + bankAccountID: number; + + /** The policy name — undefined means personal account */ + policyName?: string; +}; + +function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined, bankAccountList: BankAccountList | undefined) { + const lockedBankAccounts: LockedBankAccount[] = []; + const workspaceLockedBankAccountIDs = new Set(); + + for (const policy of adminPolicies ?? []) { + if (policy.achAccount?.state === CONST.BANK_ACCOUNT.STATE.LOCKED && policy.achAccount.bankAccountID) { + lockedBankAccounts.push({bankAccountID: policy.achAccount.bankAccountID, policyName: policy.name}); + workspaceLockedBankAccountIDs.add(policy.achAccount.bankAccountID); + } + } + + for (const account of Object.values(bankAccountList ?? {})) { + const {bankAccountID, state} = account.accountData ?? {}; + if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !workspaceLockedBankAccountIDs.has(bankAccountID)) { + lockedBankAccounts.push({bankAccountID}); + } + } + + return { + lockedBankAccounts, + }; +} + +export default useTimeSensitiveLockedBankAccount; diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index 20bc6951ae1b..8fda3f999635 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -14,13 +14,13 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {hasSynchronizationErrorMessage, isConnectionInProgress} from '@libs/actions/connections'; import {isCurrentUserValidated} from '@libs/UserUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy} from '@src/types/onyx'; import type {ConnectionName, PolicyConnectionName} from '@src/types/onyx/Policy'; import useTimeSensitiveAddPaymentCard from './hooks/useTimeSensitiveAddPaymentCard'; import useTimeSensitiveBilling from './hooks/useTimeSensitiveBilling'; import useTimeSensitiveCards from './hooks/useTimeSensitiveCards'; +import useTimeSensitiveLockedBankAccount from './hooks/useTimeSensitiveLockedBankAccount'; import ActivateCard from './items/ActivateCard'; import AddPaymentCard from './items/AddPaymentCard'; import AddShippingAddress from './items/AddShippingAddress'; @@ -59,14 +59,6 @@ type BrokenPersonalCardConnection = { cardID: string; }; -type LockedBankAccount = { - /** The ID of the locked bank account */ - bankAccountID: number; - - /** The policy name — undefined means personal account */ - policyName?: string; -}; - function TimeSensitiveSection() { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -91,6 +83,7 @@ function TimeSensitiveSection() { const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const {lockedBankAccounts} = useTimeSensitiveLockedBankAccount(adminPolicies, bankAccountList); // Get card feed errors for company card connections (Release 4) const cardFeedErrors = useCardFeedErrors(); @@ -154,22 +147,6 @@ function TimeSensitiveSection() { } } - // Find locked bank accounts — workspace VBAs (admin policies with locked achAccount) and personal accounts - const lockedBankAccounts: LockedBankAccount[] = []; - const workspaceLockedBankAccountIDs = new Set(); - for (const policy of adminPolicies ?? []) { - if (policy.achAccount?.state === CONST.BANK_ACCOUNT.STATE.LOCKED && policy.achAccount.bankAccountID) { - lockedBankAccounts.push({bankAccountID: policy.achAccount.bankAccountID, policyName: policy.name}); - workspaceLockedBankAccountIDs.add(policy.achAccount.bankAccountID); - } - } - for (const account of Object.values(bankAccountList ?? {})) { - const {bankAccountID, state} = account.accountData ?? {}; - if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !workspaceLockedBankAccountIDs.has(bankAccountID)) { - lockedBankAccounts.push({bankAccountID}); - } - } - const hasBrokenCompanyCards = brokenCompanyCardConnections.length > 0; const hasBrokenPersonalCards = brokenPersonalCardConnections.length > 0; const hasBrokenAccountingConnections = brokenAccountingConnections.length > 0; From c7382b23455143a8e2e87f960b979e2d212f32db Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 21 Apr 2026 14:56:53 +0200 Subject: [PATCH 07/23] Add Spanish translations for locked bank account widget --- src/languages/es.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index 97f4ac09c34a..2f8f640a6c7e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -901,6 +901,12 @@ const translations: TranslationDeepObject = { title: 'No pudimos cobrar a la tarjeta registrada.', subtitle: 'Suscripción', }, + unlockBankAccount: { + workspaceTitle: 'Tu cuenta bancaria empresarial ha sido bloqueada', + personalTitle: 'Tu cuenta bancaria ha sido bloqueada', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, + personalSubtitle: 'Monedero', + }, }, freeTrialSection: { title: ({days}: {days: number}) => `Prueba gratuita: ${days} ${days === 1 ? 'día' : 'días'} restantes!`, From 0cfaf89a2d2b0b2c1786f351f117d88977ece052 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 21 Apr 2026 15:16:00 +0200 Subject: [PATCH 08/23] Refactor locked bank account hook to source Onyx data --- .../hooks/useTimeSensitiveLockedBankAccount.ts | 8 ++++++-- src/pages/home/TimeSensitiveSection/index.tsx | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts index fa29597f02e4..45c4247e83ab 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts @@ -1,5 +1,8 @@ +import useOnyx from '@hooks/useOnyx'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {BankAccountList, Policy} from '@src/types/onyx'; +import {getEmptyObject} from '@src/types/utils/EmptyObject'; type LockedBankAccount = { /** The ID of the locked bank account */ @@ -9,7 +12,8 @@ type LockedBankAccount = { policyName?: string; }; -function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined, bankAccountList: BankAccountList | undefined) { +function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) { + const [bankAccountList = getEmptyObject()] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const lockedBankAccounts: LockedBankAccount[] = []; const workspaceLockedBankAccountIDs = new Set(); @@ -20,7 +24,7 @@ function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined, } } - for (const account of Object.values(bankAccountList ?? {})) { + for (const account of Object.values(bankAccountList)) { const {bankAccountID, state} = account.accountData ?? {}; if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !workspaceLockedBankAccountIDs.has(bankAccountID)) { lockedBankAccounts.push({bankAccountID}); diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index 8fda3f999635..94a75c9f3e81 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -82,8 +82,7 @@ function TimeSensitiveSection() { }); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); - const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); - const {lockedBankAccounts} = useTimeSensitiveLockedBankAccount(adminPolicies, bankAccountList); + const {lockedBankAccounts} = useTimeSensitiveLockedBankAccount(adminPolicies); // Get card feed errors for company card connections (Release 4) const cardFeedErrors = useCardFeedErrors(); From c06d290a2e25d85c398ea422b373411cbb76a6bc Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 21 Apr 2026 15:20:15 +0200 Subject: [PATCH 09/23] Update bank-lock SVG compression --- assets/images/bank-lock.svg | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/assets/images/bank-lock.svg b/assets/images/bank-lock.svg index 4a0b4f19dc4a..fe1fc4268fb1 100644 --- a/assets/images/bank-lock.svg +++ b/assets/images/bank-lock.svg @@ -1,5 +1 @@ - - - - - \ No newline at end of file + \ No newline at end of file From 0b1e2eb9cc24ca21618d08224f46b445a5e25d0b Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 21 Apr 2026 18:06:35 +0200 Subject: [PATCH 10/23] Fix es translation --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 2f8f640a6c7e..b414c4683757 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -864,7 +864,7 @@ const translations: TranslationDeepObject = { }, fixPersonalCardConnection: { title: ({cardName}: {cardName?: string}) => (cardName ? `Arreglar la conexión de la tarjeta personal de ${cardName}` : 'Arreglar la conexión de la tarjeta personal'), - subtitle: 'Monedero', + subtitle: 'Billetera', }, fixAccountingConnection: { title: ({integrationName}: {integrationName: string}) => `Reconectar con ${integrationName}`, @@ -905,7 +905,7 @@ const translations: TranslationDeepObject = { workspaceTitle: 'Tu cuenta bancaria empresarial ha sido bloqueada', personalTitle: 'Tu cuenta bancaria ha sido bloqueada', workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, - personalSubtitle: 'Monedero', + personalSubtitle: 'Billetera', }, }, freeTrialSection: { From 799930e5c2daa2aaae0b511bc58c797fd35cdbfd Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 22 Apr 2026 15:02:37 +0200 Subject: [PATCH 11/23] Fix locked bank account widget gating for reimbursers --- .../hooks/useTimeSensitiveLockedBankAccount.ts | 16 +++++++++++++--- src/pages/home/TimeSensitiveSection/index.tsx | 8 ++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts index 45c4247e83ab..e6e10e9f4305 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts @@ -1,3 +1,4 @@ +import {primaryLoginSelector} from '@selectors/Account'; import useOnyx from '@hooks/useOnyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -14,14 +15,23 @@ type LockedBankAccount = { function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) { const [bankAccountList = getEmptyObject()] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: primaryLoginSelector}); const lockedBankAccounts: LockedBankAccount[] = []; const workspaceLockedBankAccountIDs = new Set(); for (const policy of adminPolicies ?? []) { - if (policy.achAccount?.state === CONST.BANK_ACCOUNT.STATE.LOCKED && policy.achAccount.bankAccountID) { - lockedBankAccounts.push({bankAccountID: policy.achAccount.bankAccountID, policyName: policy.name}); - workspaceLockedBankAccountIDs.add(policy.achAccount.bankAccountID); + const achAccount = policy.achAccount; + if (achAccount?.state !== CONST.BANK_ACCOUNT.STATE.LOCKED || !achAccount.bankAccountID) { + continue; } + + workspaceLockedBankAccountIDs.add(achAccount.bankAccountID); + const isCurrentUserReimburser = !!primaryLogin && achAccount.reimburser === primaryLogin; + if (!isCurrentUserReimburser) { + continue; + } + + lockedBankAccounts.push({bankAccountID: achAccount.bankAccountID, policyName: policy.name}); } for (const account of Object.values(bankAccountList)) { diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index 94a75c9f3e81..26f8c75fa0a4 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -237,11 +237,11 @@ function TimeSensitiveSection() { })} {/* Priority 7: Locked bank accounts */} - {lockedBankAccounts.map((account) => ( + {lockedBankAccounts.map((lockedBankAccount) => ( ))} From 5cf022e5507792b6a4af878b569bcf20d3529019 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 22 Apr 2026 15:11:24 +0200 Subject: [PATCH 12/23] Fix null-safe locked bank account iteration --- .../hooks/useTimeSensitiveLockedBankAccount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts index e6e10e9f4305..db7da48ef405 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts @@ -35,7 +35,7 @@ function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) } for (const account of Object.values(bankAccountList)) { - const {bankAccountID, state} = account.accountData ?? {}; + const {bankAccountID, state} = account?.accountData ?? {}; if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !workspaceLockedBankAccountIDs.has(bankAccountID)) { lockedBankAccounts.push({bankAccountID}); } From eab18e2d3bace54b9fa4be5b5984d5f7ea71464a Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 22 Apr 2026 15:20:34 +0200 Subject: [PATCH 13/23] Fix personal locked bank account type filtering --- .../useTimeSensitiveLockedBankAccount.ts | 8 ++++++- .../UnlockBankAccountTest.tsx | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts index db7da48ef405..439c1cba5bcc 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts @@ -35,7 +35,13 @@ function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) } for (const account of Object.values(bankAccountList)) { - const {bankAccountID, state} = account?.accountData ?? {}; + const {bankAccountID, state, type} = account?.accountData ?? {}; + const isPersonalAccount = type === undefined || type === CONST.BANK_ACCOUNT.TYPE.PERSONAL; + + if (!isPersonalAccount) { + continue; + } + if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !workspaceLockedBankAccountIDs.has(bankAccountID)) { lockedBankAccounts.push({bankAccountID}); } diff --git a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx index eeaf92f1f994..1d1a3ab06b46 100644 --- a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx +++ b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx @@ -73,6 +73,7 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { beforeEach(async () => { await Onyx.clear(); + await Onyx.set(ONYXKEYS.ACCOUNT, {primaryLogin: 'admin@example.com'}); await waitForBatchedUpdates(); }); @@ -110,6 +111,7 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { accountData: { bankAccountID: LOCKED_BANK_ACCOUNT_ID, state: CONST.BANK_ACCOUNT.STATE.LOCKED, + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, }, }, }); @@ -120,6 +122,26 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { expect(screen.getByText('homePage.timeSensitiveSection.unlockBankAccount.personalTitle')).toBeTruthy(); }); + it('does NOT render personal UnlockBankAccount for locked business bank accounts', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, { + [`bankAccount-${LOCKED_BANK_ACCOUNT_ID}`]: { + bankCurrency: 'USD', + bankCountry: 'US', + accountData: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, + }, + }, + }); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + expect(screen.queryByText('homePage.timeSensitiveSection.unlockBankAccount.personalTitle')).toBeNull(); + }); + it('does NOT render UnlockBankAccount when bank account state is OPEN', async () => { await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, { From 292f1fbfa847908d84087033cdd8a96ac5087910 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 22 Apr 2026 15:26:38 +0200 Subject: [PATCH 14/23] Fix duplicate keys for locked bank account widgets --- .../useTimeSensitiveLockedBankAccount.ts | 14 ++++- src/pages/home/TimeSensitiveSection/index.tsx | 2 +- .../UnlockBankAccountTest.tsx | 53 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts index 439c1cba5bcc..fdbd3d93b7b2 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts @@ -6,6 +6,9 @@ import type {BankAccountList, Policy} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; type LockedBankAccount = { + /** Stable key used to render this account widget */ + key: string; + /** The ID of the locked bank account */ bankAccountID: number; @@ -31,7 +34,11 @@ function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) continue; } - lockedBankAccounts.push({bankAccountID: achAccount.bankAccountID, policyName: policy.name}); + lockedBankAccounts.push({ + key: `workspace-${policy.id}-${achAccount.bankAccountID}`, + bankAccountID: achAccount.bankAccountID, + policyName: policy.name, + }); } for (const account of Object.values(bankAccountList)) { @@ -43,7 +50,10 @@ function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) } if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !workspaceLockedBankAccountIDs.has(bankAccountID)) { - lockedBankAccounts.push({bankAccountID}); + lockedBankAccounts.push({ + key: `personal-${bankAccountID}`, + bankAccountID, + }); } } diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index 26f8c75fa0a4..01377df78687 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -239,7 +239,7 @@ function TimeSensitiveSection() { {/* Priority 7: Locked bank accounts */} {lockedBankAccounts.map((lockedBankAccount) => ( diff --git a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx index 1d1a3ab06b46..27d99e65e5b3 100644 --- a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx +++ b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx @@ -232,6 +232,59 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { expect(widgets).toHaveLength(2); }); + it('does not log duplicate key warnings when multiple workspaces share a locked bankAccountID', async () => { + const SECOND_POLICY_ID = 'policy_2'; + const duplicateKeyWarningMessage = 'Encountered two children with the same key'; + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + try { + await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, { + id: POLICY_ID, + name: 'Workspace One', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + isPolicyExpenseChatEnabled: true, + achAccount: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + accountNumber: 'XXXXXXXX1234', + routingNumber: '123456789', + addressName: 'Test Bank', + bankName: 'Test Bank', + reimburser: 'admin@example.com', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${SECOND_POLICY_ID}`, { + id: SECOND_POLICY_ID, + name: 'Workspace Two', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + isPolicyExpenseChatEnabled: true, + achAccount: { + bankAccountID: LOCKED_BANK_ACCOUNT_ID, + accountNumber: 'XXXXXXXX5678', + routingNumber: '987654321', + addressName: 'Another Bank', + bankName: 'Another Bank', + reimburser: 'admin@example.com', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + const widgets = screen.getAllByText('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle'); + expect(widgets).toHaveLength(2); + + const duplicateKeyWarnings = consoleErrorSpy.mock.calls.flat().filter((arg) => typeof arg === 'string' && arg.includes(duplicateKeyWarningMessage)); + expect(duplicateKeyWarnings).toHaveLength(0); + } finally { + consoleErrorSpy.mockRestore(); + } + }); + it('calls pressLockedBankAccount and navigates to Concierge when CTA is pressed', async () => { await Onyx.set(ONYXKEYS.SESSION, {email: 'admin@example.com', accountID: ADMIN_ACCOUNT_ID}); await Onyx.set(ONYXKEYS.CONCIERGE_REPORT_ID, CONCIERGE_REPORT_ID); From 79402798f08cd68d3706ffcd013b69480d03736f Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 22 Apr 2026 15:27:25 +0200 Subject: [PATCH 15/23] Add unlock bank account translations for additional locales --- src/languages/de.ts | 1 + src/languages/fr.ts | 5 +++++ src/languages/it.ts | 5 +++++ src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 8 files changed, 16 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 0c8f00926938..c0eda193d95d 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -969,6 +969,7 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Bestätigen Sie Ihr Konto, um Expensify weiter zu verwenden', subtitle: 'Konto', cta: 'Bestätigen'}, fixFailedBilling: {title: 'Wir konnten Ihre hinterlegte Karte nicht belasten', subtitle: 'Abonnement'}, + unlockBankAccount: {workspaceTitle: 'Ihr Geschäftskonto wurde gesperrt', personalTitle: 'Ihr Bankkonto wurde gesperrt', personalSubtitle: 'Geldbörse'}, }, assignedCards: 'Ihre Expensify Karten', assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} verbleibend`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 62d12fdb43af..703af7023ac4 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -972,6 +972,11 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Validez votre compte pour continuer à utiliser Expensify', subtitle: 'Compte', cta: 'Valider'}, fixFailedBilling: {title: 'Nous n’avons pas pu débiter votre carte enregistrée', subtitle: 'Abonnement'}, + unlockBankAccount: { + workspaceTitle: 'Votre compte bancaire professionnel a été verrouillé', + personalTitle: 'Votre compte bancaire a été verrouillé', + personalSubtitle: 'Portefeuille', + }, }, assignedCards: 'Vos cartes Expensify', assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restant`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 3ed65ebec7b2..73ca1db07074 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -969,6 +969,11 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Conferma il tuo account per continuare a usare Expensify', subtitle: 'Account', cta: 'Conferma'}, fixFailedBilling: {title: 'Non abbiamo potuto addebitare la carta salvata nel profilo', subtitle: 'Abbonamento'}, + unlockBankAccount: { + workspaceTitle: 'Il conto bancario della tua azienda è stato bloccato', + personalTitle: 'Il tuo conto bancario è stato bloccato', + personalSubtitle: 'Portafoglio', + }, }, assignedCards: 'Le tue Carte Expensify', assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} rimanenti`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2723e83a119a..84d824e07c82 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -952,6 +952,7 @@ const translations: TranslationDeepObject = { fixPersonalCardConnection: {title: ({cardName}: {cardName?: string}) => (cardName ? `${cardName}個人カードの接続を修正` : '個人カードの連携を修正'), subtitle: 'ウォレット'}, validateAccount: {title: 'Expensify を引き続きご利用いただくには、アカウントを認証してください', subtitle: 'アカウント', cta: '検証する'}, fixFailedBilling: {title: '登録されているカードから請求できませんでした', subtitle: 'サブスクリプション'}, + unlockBankAccount: {workspaceTitle: 'ビジネス用銀行口座がロックされました', personalTitle: 'あなたの銀行口座はロックされています', personalSubtitle: 'ウォレット'}, }, assignedCards: 'お客様の Expensify カード', assignedCardsRemaining: ({amount}: {amount: string}) => `残額:${amount}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 4c7a16b46b01..061bef7aec3c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -968,6 +968,7 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Valideer je account om Expensify te blijven gebruiken', subtitle: 'Account', cta: 'Valideren'}, fixFailedBilling: {title: 'We konden je kaart in ons bestand niet belasten', subtitle: 'Abonnement'}, + unlockBankAccount: {workspaceTitle: 'Je zakelijke bankrekening is geblokkeerd', personalTitle: 'Je bankrekening is vergrendeld', personalSubtitle: 'Portemonnee'}, }, assignedCards: 'Je Expensify Kaarten', assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} resterend`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a0291f78d160..ac3a9b82745f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -969,6 +969,7 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Zweryfikuj swoje konto, aby dalej korzystać z Expensify', subtitle: 'Konto', cta: 'Zatwierdź'}, fixFailedBilling: {title: 'Nie mogliśmy obciążyć zapisanej karty', subtitle: 'Subskrypcja'}, + unlockBankAccount: {workspaceTitle: 'Twoje firmowe konto bankowe zostało zablokowane', personalTitle: 'Twoje konto bankowe zostało zablokowane', personalSubtitle: 'Portfel'}, }, assignedCards: 'Twoje Karty Expensify', assignedCardsRemaining: ({amount}: {amount: string}) => `Pozostało ${amount}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 5b236b8b0ec4..cfd895555c1d 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -967,6 +967,7 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Valide sua conta para continuar usando o Expensify', subtitle: 'Conta', cta: 'Validar'}, fixFailedBilling: {title: 'Não foi possível cobrar o cartão cadastrado', subtitle: 'Assinatura'}, + unlockBankAccount: {workspaceTitle: 'Sua conta bancária comercial foi bloqueada', personalTitle: 'Sua conta bancária foi bloqueada', personalSubtitle: 'Carteira'}, }, assignedCards: 'Seus Cartões Expensify', assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restante`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 49c115dfe58e..e533a4e795fa 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -935,6 +935,7 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: '验证您的账户以继续使用 Expensify', subtitle: '账户', cta: '验证'}, fixFailedBilling: {title: '我们无法向您档案中的银行卡收费', subtitle: '订阅'}, + unlockBankAccount: {workspaceTitle: '您的企业银行账户已被锁定', personalTitle: '您的银行账户已被锁定', personalSubtitle: '钱包'}, }, assignedCards: '你的 Expensify 卡', assignedCardsRemaining: ({amount}: {amount: string}) => `剩余 ${amount}`, From ead2ab8ff7f996bcf89e219278de3cffab0eec68 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 22 Apr 2026 15:32:09 +0200 Subject: [PATCH 16/23] Fix locked bank account iteration when list is undefined --- .../hooks/useTimeSensitiveLockedBankAccount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts index fdbd3d93b7b2..f4bb9567b931 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts @@ -41,7 +41,7 @@ function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) }); } - for (const account of Object.values(bankAccountList)) { + for (const account of Object.values(bankAccountList ?? {})) { const {bankAccountID, state, type} = account?.accountData ?? {}; const isPersonalAccount = type === undefined || type === CONST.BANK_ACCOUNT.TYPE.PERSONAL; From 63d43366908003b9b03257bb889c1b6eaabff05f Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 22 Apr 2026 16:37:58 +0200 Subject: [PATCH 17/23] Add workspace subtitles for locked bank account locales --- src/languages/de.ts | 7 ++++++- src/languages/fr.ts | 1 + src/languages/it.ts | 1 + 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 | 7 ++++++- 8 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index c0eda193d95d..622684099c0d 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -969,7 +969,12 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Bestätigen Sie Ihr Konto, um Expensify weiter zu verwenden', subtitle: 'Konto', cta: 'Bestätigen'}, fixFailedBilling: {title: 'Wir konnten Ihre hinterlegte Karte nicht belasten', subtitle: 'Abonnement'}, - unlockBankAccount: {workspaceTitle: 'Ihr Geschäftskonto wurde gesperrt', personalTitle: 'Ihr Bankkonto wurde gesperrt', personalSubtitle: 'Geldbörse'}, + unlockBankAccount: { + workspaceTitle: 'Ihr Geschäftskonto wurde gesperrt', + personalTitle: 'Ihr Bankkonto wurde gesperrt', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, + personalSubtitle: 'Geldbörse', + }, }, assignedCards: 'Ihre Expensify Karten', assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} verbleibend`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 703af7023ac4..738a783eafa9 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -975,6 +975,7 @@ const translations: TranslationDeepObject = { unlockBankAccount: { workspaceTitle: 'Votre compte bancaire professionnel a été verrouillé', personalTitle: 'Votre compte bancaire a été verrouillé', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, personalSubtitle: 'Portefeuille', }, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 73ca1db07074..b9984b79b9aa 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -972,6 +972,7 @@ const translations: TranslationDeepObject = { unlockBankAccount: { workspaceTitle: 'Il conto bancario della tua azienda è stato bloccato', personalTitle: 'Il tuo conto bancario è stato bloccato', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, personalSubtitle: 'Portafoglio', }, }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 84d824e07c82..14059a334f8e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -952,7 +952,12 @@ const translations: TranslationDeepObject = { fixPersonalCardConnection: {title: ({cardName}: {cardName?: string}) => (cardName ? `${cardName}個人カードの接続を修正` : '個人カードの連携を修正'), subtitle: 'ウォレット'}, validateAccount: {title: 'Expensify を引き続きご利用いただくには、アカウントを認証してください', subtitle: 'アカウント', cta: '検証する'}, fixFailedBilling: {title: '登録されているカードから請求できませんでした', subtitle: 'サブスクリプション'}, - unlockBankAccount: {workspaceTitle: 'ビジネス用銀行口座がロックされました', personalTitle: 'あなたの銀行口座はロックされています', personalSubtitle: 'ウォレット'}, + unlockBankAccount: { + workspaceTitle: 'ビジネス用銀行口座がロックされました', + personalTitle: 'あなたの銀行口座はロックされています', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, + personalSubtitle: 'ウォレット', + }, }, assignedCards: 'お客様の Expensify カード', assignedCardsRemaining: ({amount}: {amount: string}) => `残額:${amount}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 061bef7aec3c..8250f29af27c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -968,7 +968,12 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Valideer je account om Expensify te blijven gebruiken', subtitle: 'Account', cta: 'Valideren'}, fixFailedBilling: {title: 'We konden je kaart in ons bestand niet belasten', subtitle: 'Abonnement'}, - unlockBankAccount: {workspaceTitle: 'Je zakelijke bankrekening is geblokkeerd', personalTitle: 'Je bankrekening is vergrendeld', personalSubtitle: 'Portemonnee'}, + unlockBankAccount: { + workspaceTitle: 'Je zakelijke bankrekening is geblokkeerd', + personalTitle: 'Je bankrekening is vergrendeld', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, + personalSubtitle: 'Portemonnee', + }, }, assignedCards: 'Je Expensify Kaarten', assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} resterend`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ac3a9b82745f..a5dd8a10605e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -969,7 +969,12 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Zweryfikuj swoje konto, aby dalej korzystać z Expensify', subtitle: 'Konto', cta: 'Zatwierdź'}, fixFailedBilling: {title: 'Nie mogliśmy obciążyć zapisanej karty', subtitle: 'Subskrypcja'}, - unlockBankAccount: {workspaceTitle: 'Twoje firmowe konto bankowe zostało zablokowane', personalTitle: 'Twoje konto bankowe zostało zablokowane', personalSubtitle: 'Portfel'}, + unlockBankAccount: { + workspaceTitle: 'Twoje firmowe konto bankowe zostało zablokowane', + personalTitle: 'Twoje konto bankowe zostało zablokowane', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, + personalSubtitle: 'Portfel', + }, }, assignedCards: 'Twoje Karty Expensify', assignedCardsRemaining: ({amount}: {amount: string}) => `Pozostało ${amount}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index cfd895555c1d..86dc9be3eb7f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -967,7 +967,12 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: 'Valide sua conta para continuar usando o Expensify', subtitle: 'Conta', cta: 'Validar'}, fixFailedBilling: {title: 'Não foi possível cobrar o cartão cadastrado', subtitle: 'Assinatura'}, - unlockBankAccount: {workspaceTitle: 'Sua conta bancária comercial foi bloqueada', personalTitle: 'Sua conta bancária foi bloqueada', personalSubtitle: 'Carteira'}, + unlockBankAccount: { + workspaceTitle: 'Sua conta bancária comercial foi bloqueada', + personalTitle: 'Sua conta bancária foi bloqueada', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, + personalSubtitle: 'Carteira', + }, }, assignedCards: 'Seus Cartões Expensify', assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restante`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index e533a4e795fa..a4f8b4a491e5 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -935,7 +935,12 @@ const translations: TranslationDeepObject = { }, validateAccount: {title: '验证您的账户以继续使用 Expensify', subtitle: '账户', cta: '验证'}, fixFailedBilling: {title: '我们无法向您档案中的银行卡收费', subtitle: '订阅'}, - unlockBankAccount: {workspaceTitle: '您的企业银行账户已被锁定', personalTitle: '您的银行账户已被锁定', personalSubtitle: '钱包'}, + unlockBankAccount: { + workspaceTitle: '您的企业银行账户已被锁定', + personalTitle: '您的银行账户已被锁定', + workspaceSubtitle: ({policyName}: {policyName: string}) => policyName, + personalSubtitle: '钱包', + }, }, assignedCards: '你的 Expensify 卡', assignedCardsRemaining: ({amount}: {amount: string}) => `剩余 ${amount}`, From 928ff871001b570f6a63422ee54cbf1273a9f342 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 24 Apr 2026 11:50:42 +0200 Subject: [PATCH 18/23] Fix namespace imports in UnlockBankAccountTest to use named imports --- ...ck_intent_getting_started_38a6098f.plan.md | 186 ++++++++++++++++++ .../UnlockBankAccountTest.tsx | 8 +- 2 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 .cursor/plans/track_intent_getting_started_38a6098f.plan.md diff --git a/.cursor/plans/track_intent_getting_started_38a6098f.plan.md b/.cursor/plans/track_intent_getting_started_38a6098f.plan.md new file mode 100644 index 000000000000..310fba47fd7d --- /dev/null +++ b/.cursor/plans/track_intent_getting_started_38a6098f.plan.md @@ -0,0 +1,186 @@ +--- +name: Track intent Getting Started +overview: Extend the Home "Getting started" section to support the `Track and budget my expenses` onboarding intent with three to-dos (Create workspace always-checked, Customize accounting categories, Invite your accountant) and apply the Track-only mobile ordering swap (For you 3rd, Getting started 4th). +todos: + - id: tests-hook + content: Add failing tests in useGettingStartedItems.test.ts for TRACK_WORKSPACE visibility, three items, category/member check logic, and routing targets; keep MANAGE_TEAM regression tests + status: completed + - id: tests-ordering + content: "Add failing test(s) covering mobile ordering: TRACK_WORKSPACE renders ForYou before GettingStarted; MANAGE_TEAM keeps existing order" + status: completed + - id: impl-hook + content: "Extend useGettingStartedItems.ts with a TRACK_WORKSPACE branch (3 items: createWorkspace always checked, customizeCategories using hasCustomCategories, inviteAccountant using employeeList count >= 2) reusing isWithinGettingStartedPeriod and isPolicyAdmin" + status: completed + - id: impl-ordering + content: Update HomePage.tsx mobile branch to swap GettingStartedSection and ForYouSection only when introSelected.choice === TRACK_WORKSPACE + status: completed + - id: impl-i18n + content: Add `inviteAccountant` key to homePage.gettingStartedSection in src/languages/en.ts and src/languages/es.ts only, and verify typecheck passes + status: completed + - id: polish + content: Run prettier, eslint, typecheck-tsgo, and react-compiler-compliance-check on changed files; ensure all new tests pass and no regressions + status: completed +isProject: false +--- + +## Issue Summary + +- URL: [https://github.com/Expensify/App/issues/88159](https://github.com/Expensify/App/issues/88159) +- Goal: Add a Home "Getting started" onboarding slot for the `Track and budget my expenses` (backend: `TRACK_WORKSPACE`) intent, with 3 to-dos and a Track-specific mobile ordering, reusing the same section component that already supports the `Manage team` intent. +- In-scope (explicit): + - Show the existing "Getting started" slot titled "Getting started" (already localized). + - Desktop: right column, 2nd spot under Free Trial (already matches current layout, no change). + - Mobile: Track intent only - put `Getting started` in position 4, `For you` in position 3. + - Visibility: only during new sign-ups and active trials; hide after 60 days from trial start. + - Only for the Track intent (`introSelected?.choice === TRACK_WORKSPACE`). + - Three to-dos: + 1. `Create a workspace` - always checked (no action-state tracking). + 2. `Customize accounting categories` - navigate to `ROUTES.WORKSPACE_CATEGORIES`, auto-checked when the active workspace has >=1 non-default category. + 3. `Invite your accountant` - navigate to `ROUTES.WORKSPACE_MEMBERS`, auto-checked when the active workspace has >=2 members in `policy.employeeList`. +- Out-of-scope (explicit): + - No persisted/saved state for checked/unchecked (purely visual). + - No change to existing Manage team ordering or behavior. + - No new backend/API changes; reuse existing Onyx keys and routes. + +## Explicit Requirements (from issue) + +1. Slot title = `Getting started` (existing translation `homePage.gettingStartedSection.title`). +2. Desktop: right column, 2nd spot (below Free Trial slot). +3. Mobile: 4th position (below `For you` in 3rd).L +4. Applies only to new sign-ups and active trials; hide after 60 days if tasks unfinished. +5. Applies only to the "track and budget my expenses" intent. +6. Clicking anywhere on a to-do row navigates to the feature route. +7. `Create a workspace` - always checked for Track intent. +8. `Customize accounting categories` - navigate to `/categories`; check when policy has >=1 non-default category. +9. `Invite your accountant` - navigate to `/members`; check when policy has >=2 members. + +## Confirmed Decisions (from user) + +- Mobile ordering: Apply the `For you`(3rd) / `Getting started`(4th) swap ONLY when intent is `TRACK_WORKSPACE` (keep existing order for Manage team). +- Workspace source: `NVP_ACTIVE_POLICY_ID` (same as Manage team). +- Intent detection: treat intent as `TRACK_WORKSPACE` only. + +## TDD Plan + +### Phase 1 - Tests First (must fail initially) + +Add new describe block `TRACK_WORKSPACE intent` in `[tests/unit/hooks/useGettingStartedItems.test.ts](tests/unit/hooks/useGettingStartedItems.test.ts)`: + +- Visibility rules: + - Returns empty when intent is `TRACK_WORKSPACE` but `NVP_ACTIVE_POLICY_ID` is missing. + - Returns empty when intent is `TRACK_WORKSPACE`, policy exists, but more than 60 days have passed since `NVP_FIRST_DAY_FREE_TRIAL`. + - Returns empty when intent is `TRACK_WORKSPACE` and user is not a policy admin. + - Returns empty when intent is `TRACK_WORKSPACE` and the active policy is not a paid group policy (e.g. personal policy). + - Returns items when intent is `TRACK_WORKSPACE`, within 60 days, policy admin on a paid group policy. + - Returns items when `introSelected?.choice` is undefined but `onboardingPurpose === TRACK_WORKSPACE` (fallback parity with Manage team). +- Items and check states (`shouldShowSection = true`, three items in order `createWorkspace`, `customizeCategories`, `inviteAccountant`): + - `createWorkspace` is always `isComplete: true` even when no workspace exists/pending. + - `customizeCategories` route resolves to `ROUTES.WORKSPACE_CATEGORIES.getRoute(POLICY_ID)`. + - `customizeCategories` `isComplete: false` with only default categories; `true` with a non-default category in `POLICY_CATEGORIES`. + - `customizeCategories` is still rendered (not replaced by `connectAccounting`) when the policy has an accounting integration connected - Track branch skips the `isAccountingEnabled` split. + - `createWorkspace` route resolves to `ROUTES.WORKSPACE_INITIAL.getRoute(POLICY_ID, ...)` on narrow layout and `ROUTES.WORKSPACE_OVERVIEW.getRoute(POLICY_ID)` on wide layout (matches Manage team). + - `inviteAccountant` route resolves to `ROUTES.WORKSPACE_MEMBERS.getRoute(POLICY_ID)`. + - `inviteAccountant` `isComplete: false` with 1 member, `true` with >=2 entries in `policy.employeeList`. + - `inviteAccountant` ignores `employeeList` entries with `pendingAction === DELETE` when counting members. +- Regression: + - Existing `MANAGE_TEAM` cases still return the manage-team items (unchanged). + +Add a minimal render test for mobile ordering. First check if a home-page test file already exists (e.g. under `tests/unit/pages/`) and extend it; only create a new file if none exists: + +- When intent is `TRACK_WORKSPACE` and narrow layout is on, `ForYouSection` renders before `GettingStartedSection`. +- When `introSelected?.choice` is undefined but `onboardingPurpose === TRACK_WORKSPACE` and narrow layout is on, the swap still applies (intent source parity). +- When intent is `MANAGE_TEAM` and narrow layout is on, `GettingStartedSection` still renders before `ForYouSection` (unchanged). + +### Phase 2 - Minimal Implementation + +1. Extend `[src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts](src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts)`: + - Replace the single-intent early return with an intent switch; if `intent` is neither `MANAGE_TEAM` nor `TRACK_WORKSPACE`, return empty. Intent resolution stays `introSelected?.choice ?? onboardingPurpose`. + - For `TRACK_WORKSPACE`: + - Reuse the same guard stack as Manage team: `activePolicyID` present, `policy` present, `!isPendingDeletePolicy(policy)`, `isPaidGroupPolicy(policy)`, `isPolicyAdmin(policy)`. + - Reuse `isWithinGettingStartedPeriod(firstDayFreeTrial)` for the 60-day/active-trial window. + - Track branch always builds `customizeCategories` - it does NOT go through the `isAccountingEnabled` -> `connectAccounting` split that Manage team uses. + - Build items (exactly three, in order): + - `createWorkspace`: `isComplete: true`, route `shouldUseNarrowLayout ? ROUTES.WORKSPACE_INITIAL.getRoute(activePolicyID, Navigation.getActiveRoute()) : ROUTES.WORKSPACE_OVERVIEW.getRoute(activePolicyID)` (matches Manage team pattern). + - `customizeCategories`: `isComplete: hasCustomCategories(policyCategories)`, route `ROUTES.WORKSPACE_CATEGORIES.getRoute(activePolicyID)`, with `isFeatureEnabled: policy.areCategoriesEnabled` and `enableFeature` calling `enablePolicyCategories` so tapping the row auto-enables categories (mirrors Manage team logic). + - `inviteAccountant`: `isComplete: Object.values(policy?.employeeList ?? {}).filter((member) => member?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length >= 2`, route `ROUTES.WORKSPACE_MEMBERS.getRoute(activePolicyID)`. + - Keep `MANAGE_TEAM` branch untouched. + - Optional refactor: extract a shared `useOnboardingIntent()` hook returning `introSelected?.choice ?? onboardingPurpose`, and use it in both this hook and `HomePage.tsx`. +2. Add translation key `inviteAccountant` to `homePage.gettingStartedSection` in EN and ES only: + - `[src/languages/en.ts](src/languages/en.ts)`: `inviteAccountant: 'Invite your accountant'`. + - `[src/languages/es.ts](src/languages/es.ts)`: generate the Spanish translation (JaimeGPT-style) and add it to the matching section. + - Do NOT edit other locale files in this PR. +3. Update `[src/pages/home/HomePage.tsx](src/pages/home/HomePage.tsx)` mobile layout: + - Read both `introSelected` (via `useOnyx(ONYXKEYS.NVP_INTRO_SELECTED)`) and `onboardingPurpose` (via `useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED)`), or use the shared `useOnboardingIntent()` helper if added. Compute `intent = introSelected?.choice ?? onboardingPurpose` so this stays in lockstep with `useGettingStartedItems`. + - In the narrow-layout branch, when `intent === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE`, render `ForYouSection` before `GettingStartedSection`. Otherwise preserve the existing order (`GettingStartedSection` then `ForYouSection`). + - Desktop right-column order is unchanged. + +### Phase 3 - Refactor + +- If the intent branching in `useGettingStartedItems.ts` grows, extract per-intent builders (e.g., `buildManageTeamItems`, `buildTrackWorkspaceItems`) for readability - no behavior change. +- Ensure `GettingStartedRow`'s existing `navigateToItem` handles the `createWorkspace`-always-checked row gracefully (current rendering hides the arrow icon when complete; confirm via snapshot/interaction test). + +### Phase 4 - Regression and Edge Cases + +- Custom category with `pendingAction === DELETE` does not count (already handled by `hasCustomCategories`). +- Default categories from `CONST.POLICY.DEFAULT_CATEGORIES` do not count. +- Member count uses `policy.employeeList` keys; verify owner-only policy returns 1 member and stays unchecked. +- Add tests for the 60-day boundary: day 60 inclusive shows, day 61 hides (matches `SIXTY_DAYS_MS`). +- Verify no regression to mobile ordering for non-Track intents by ensuring the default branch runs. + +## Decision Log (No Hidden Decisions) + +1. Decision: Mobile ordering swap is conditional on intent. + - Options considered: (a) conditional on Track, (b) universal swap, (c) no change. + - Chosen option: (a) conditional on Track. + - Source: User answer. + - Confirmation needed: No. +2. Decision: Workspace used for checks and navigation is `NVP_ACTIVE_POLICY_ID`. + - Options considered: active policy, first owned paid policy, onboarding policy ID. + - Chosen option: `NVP_ACTIVE_POLICY_ID`. + - Source: User answer. + - Confirmation needed: No. +3. Decision: Intent detection uses `TRACK_WORKSPACE` only. + - Options considered: `TRACK_WORKSPACE` only, or include `PERSONAL_SPEND`. + - Chosen option: `TRACK_WORKSPACE` only. + - Source: User answer. + - Confirmation needed: No. +4. Decision: Reuse `isWithinGettingStartedPeriod(firstDayFreeTrial)` (60-day window from trial start) for Track visibility. + - Options considered: New helper mirroring `shouldShowTrialEndedUI`, or reuse existing helper. + - Chosen option: Reuse existing helper - it already encodes "within 60 days from trial start" which covers both new sign-ups and active trials as specified in the issue. + - Source: Cross-validation of current `useGettingStartedItems` implementation; matches Manage team semantics. + - Confirmation needed: No. +5. Decision: Enforce `isPaidGroupPolicy` guard for Track (same guard stack as Manage team). + - Options considered: Enforce paid group policy, or allow any admin policy. + - Chosen option: Enforce paid group policy - consistent with Manage team, avoids personal-policy edge cases where `areCategoriesEnabled`/`enablePolicyCategories` do not apply. The Track workspace auto-created by `autoCreateTrackWorkspace` is a group policy so this does not exclude valid Track users. + - Source: Cross-validation. + - Confirmation needed: No. +6. Decision: `Create a workspace` row remains visually always-checked, navigating to the active workspace overview (matches Manage team behavior). + - Options considered: Hide the row entirely, or always checked. + - Chosen option: Always checked (issue explicitly calls this out). + - Source: Issue. + - Confirmation needed: No. +7. Decision: Intent source in `HomePage.tsx` mobile ordering matches `useGettingStartedItems` - `introSelected?.choice ?? onboardingPurpose`. + - Options considered: Use `introSelected?.choice` only, or mirror the hook's fallback. + - Chosen option: Mirror the hook's fallback to avoid transient mismatch where items render as Track but section order does not swap. + - Source: Cross-validation. + - Confirmation needed: No. + +## Validation Checklist + +- Every plan step maps to an explicit requirement or confirmed decision. +- Tests are listed before implementation tasks (TDD). +- All decisions (1-7) are finalized; no pending confirmations. +- Open questions were raised and answered before finalizing. +- No changes to Manage team behavior or desktop layout. +- EN and ES locale files updated with the new `inviteAccountant` key (CI will fail otherwise). Other locales intentionally left for a follow-up. + +## Files Touched + +- [src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts](src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts) +- [src/pages/home/HomePage.tsx](src/pages/home/HomePage.tsx) +- [src/languages/en.ts](src/languages/en.ts) +- [src/languages/es.ts](src/languages/es.ts) +- [tests/unit/hooks/useGettingStartedItems.test.ts](tests/unit/hooks/useGettingStartedItems.test.ts) +- (optional) new [tests/unit/pages/HomePage.test.tsx](tests/unit/pages/HomePage.test.tsx) for mobile ordering (extend an existing home-page test file if present) +- (optional) new shared hook `src/hooks/useOnboardingIntent.ts` if the intent-resolution refactor is taken + diff --git a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx index 27d99e65e5b3..fcdf19273aeb 100644 --- a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx +++ b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx @@ -1,7 +1,7 @@ import {fireEvent, render, screen} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; -import * as BankAccountsActions from '@libs/actions/BankAccounts'; -import * as ReportActions from '@libs/actions/Report'; +import {pressLockedBankAccount} from '@libs/actions/BankAccounts'; +import {navigateToConciergeChat} from '@libs/actions/Report'; import OnyxListItemProvider from '@src/components/OnyxListItemProvider'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -311,7 +311,7 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { const cta = screen.getByText('homePage.timeSensitiveSection.ctaFix'); fireEvent.press(cta); - expect(BankAccountsActions.pressLockedBankAccount).toHaveBeenCalledWith(LOCKED_BANK_ACCOUNT_ID, expect.any(Function), CONCIERGE_REPORT_ID); - expect(ReportActions.navigateToConciergeChat).toHaveBeenCalledWith(CONCIERGE_REPORT_ID, undefined, ADMIN_ACCOUNT_ID, false, undefined); + expect(pressLockedBankAccount).toHaveBeenCalledWith(LOCKED_BANK_ACCOUNT_ID, expect.any(Function), CONCIERGE_REPORT_ID); + expect(navigateToConciergeChat).toHaveBeenCalledWith(CONCIERGE_REPORT_ID, undefined, ADMIN_ACCOUNT_ID, false, undefined); }); }); From 7f88888a7aa8357df63b421eaeb7b9dee7e04c66 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 24 Apr 2026 12:00:45 +0200 Subject: [PATCH 19/23] Remove .cursor --- ...ck_intent_getting_started_38a6098f.plan.md | 186 ------------------ 1 file changed, 186 deletions(-) delete mode 100644 .cursor/plans/track_intent_getting_started_38a6098f.plan.md diff --git a/.cursor/plans/track_intent_getting_started_38a6098f.plan.md b/.cursor/plans/track_intent_getting_started_38a6098f.plan.md deleted file mode 100644 index 310fba47fd7d..000000000000 --- a/.cursor/plans/track_intent_getting_started_38a6098f.plan.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -name: Track intent Getting Started -overview: Extend the Home "Getting started" section to support the `Track and budget my expenses` onboarding intent with three to-dos (Create workspace always-checked, Customize accounting categories, Invite your accountant) and apply the Track-only mobile ordering swap (For you 3rd, Getting started 4th). -todos: - - id: tests-hook - content: Add failing tests in useGettingStartedItems.test.ts for TRACK_WORKSPACE visibility, three items, category/member check logic, and routing targets; keep MANAGE_TEAM regression tests - status: completed - - id: tests-ordering - content: "Add failing test(s) covering mobile ordering: TRACK_WORKSPACE renders ForYou before GettingStarted; MANAGE_TEAM keeps existing order" - status: completed - - id: impl-hook - content: "Extend useGettingStartedItems.ts with a TRACK_WORKSPACE branch (3 items: createWorkspace always checked, customizeCategories using hasCustomCategories, inviteAccountant using employeeList count >= 2) reusing isWithinGettingStartedPeriod and isPolicyAdmin" - status: completed - - id: impl-ordering - content: Update HomePage.tsx mobile branch to swap GettingStartedSection and ForYouSection only when introSelected.choice === TRACK_WORKSPACE - status: completed - - id: impl-i18n - content: Add `inviteAccountant` key to homePage.gettingStartedSection in src/languages/en.ts and src/languages/es.ts only, and verify typecheck passes - status: completed - - id: polish - content: Run prettier, eslint, typecheck-tsgo, and react-compiler-compliance-check on changed files; ensure all new tests pass and no regressions - status: completed -isProject: false ---- - -## Issue Summary - -- URL: [https://github.com/Expensify/App/issues/88159](https://github.com/Expensify/App/issues/88159) -- Goal: Add a Home "Getting started" onboarding slot for the `Track and budget my expenses` (backend: `TRACK_WORKSPACE`) intent, with 3 to-dos and a Track-specific mobile ordering, reusing the same section component that already supports the `Manage team` intent. -- In-scope (explicit): - - Show the existing "Getting started" slot titled "Getting started" (already localized). - - Desktop: right column, 2nd spot under Free Trial (already matches current layout, no change). - - Mobile: Track intent only - put `Getting started` in position 4, `For you` in position 3. - - Visibility: only during new sign-ups and active trials; hide after 60 days from trial start. - - Only for the Track intent (`introSelected?.choice === TRACK_WORKSPACE`). - - Three to-dos: - 1. `Create a workspace` - always checked (no action-state tracking). - 2. `Customize accounting categories` - navigate to `ROUTES.WORKSPACE_CATEGORIES`, auto-checked when the active workspace has >=1 non-default category. - 3. `Invite your accountant` - navigate to `ROUTES.WORKSPACE_MEMBERS`, auto-checked when the active workspace has >=2 members in `policy.employeeList`. -- Out-of-scope (explicit): - - No persisted/saved state for checked/unchecked (purely visual). - - No change to existing Manage team ordering or behavior. - - No new backend/API changes; reuse existing Onyx keys and routes. - -## Explicit Requirements (from issue) - -1. Slot title = `Getting started` (existing translation `homePage.gettingStartedSection.title`). -2. Desktop: right column, 2nd spot (below Free Trial slot). -3. Mobile: 4th position (below `For you` in 3rd).L -4. Applies only to new sign-ups and active trials; hide after 60 days if tasks unfinished. -5. Applies only to the "track and budget my expenses" intent. -6. Clicking anywhere on a to-do row navigates to the feature route. -7. `Create a workspace` - always checked for Track intent. -8. `Customize accounting categories` - navigate to `/categories`; check when policy has >=1 non-default category. -9. `Invite your accountant` - navigate to `/members`; check when policy has >=2 members. - -## Confirmed Decisions (from user) - -- Mobile ordering: Apply the `For you`(3rd) / `Getting started`(4th) swap ONLY when intent is `TRACK_WORKSPACE` (keep existing order for Manage team). -- Workspace source: `NVP_ACTIVE_POLICY_ID` (same as Manage team). -- Intent detection: treat intent as `TRACK_WORKSPACE` only. - -## TDD Plan - -### Phase 1 - Tests First (must fail initially) - -Add new describe block `TRACK_WORKSPACE intent` in `[tests/unit/hooks/useGettingStartedItems.test.ts](tests/unit/hooks/useGettingStartedItems.test.ts)`: - -- Visibility rules: - - Returns empty when intent is `TRACK_WORKSPACE` but `NVP_ACTIVE_POLICY_ID` is missing. - - Returns empty when intent is `TRACK_WORKSPACE`, policy exists, but more than 60 days have passed since `NVP_FIRST_DAY_FREE_TRIAL`. - - Returns empty when intent is `TRACK_WORKSPACE` and user is not a policy admin. - - Returns empty when intent is `TRACK_WORKSPACE` and the active policy is not a paid group policy (e.g. personal policy). - - Returns items when intent is `TRACK_WORKSPACE`, within 60 days, policy admin on a paid group policy. - - Returns items when `introSelected?.choice` is undefined but `onboardingPurpose === TRACK_WORKSPACE` (fallback parity with Manage team). -- Items and check states (`shouldShowSection = true`, three items in order `createWorkspace`, `customizeCategories`, `inviteAccountant`): - - `createWorkspace` is always `isComplete: true` even when no workspace exists/pending. - - `customizeCategories` route resolves to `ROUTES.WORKSPACE_CATEGORIES.getRoute(POLICY_ID)`. - - `customizeCategories` `isComplete: false` with only default categories; `true` with a non-default category in `POLICY_CATEGORIES`. - - `customizeCategories` is still rendered (not replaced by `connectAccounting`) when the policy has an accounting integration connected - Track branch skips the `isAccountingEnabled` split. - - `createWorkspace` route resolves to `ROUTES.WORKSPACE_INITIAL.getRoute(POLICY_ID, ...)` on narrow layout and `ROUTES.WORKSPACE_OVERVIEW.getRoute(POLICY_ID)` on wide layout (matches Manage team). - - `inviteAccountant` route resolves to `ROUTES.WORKSPACE_MEMBERS.getRoute(POLICY_ID)`. - - `inviteAccountant` `isComplete: false` with 1 member, `true` with >=2 entries in `policy.employeeList`. - - `inviteAccountant` ignores `employeeList` entries with `pendingAction === DELETE` when counting members. -- Regression: - - Existing `MANAGE_TEAM` cases still return the manage-team items (unchanged). - -Add a minimal render test for mobile ordering. First check if a home-page test file already exists (e.g. under `tests/unit/pages/`) and extend it; only create a new file if none exists: - -- When intent is `TRACK_WORKSPACE` and narrow layout is on, `ForYouSection` renders before `GettingStartedSection`. -- When `introSelected?.choice` is undefined but `onboardingPurpose === TRACK_WORKSPACE` and narrow layout is on, the swap still applies (intent source parity). -- When intent is `MANAGE_TEAM` and narrow layout is on, `GettingStartedSection` still renders before `ForYouSection` (unchanged). - -### Phase 2 - Minimal Implementation - -1. Extend `[src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts](src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts)`: - - Replace the single-intent early return with an intent switch; if `intent` is neither `MANAGE_TEAM` nor `TRACK_WORKSPACE`, return empty. Intent resolution stays `introSelected?.choice ?? onboardingPurpose`. - - For `TRACK_WORKSPACE`: - - Reuse the same guard stack as Manage team: `activePolicyID` present, `policy` present, `!isPendingDeletePolicy(policy)`, `isPaidGroupPolicy(policy)`, `isPolicyAdmin(policy)`. - - Reuse `isWithinGettingStartedPeriod(firstDayFreeTrial)` for the 60-day/active-trial window. - - Track branch always builds `customizeCategories` - it does NOT go through the `isAccountingEnabled` -> `connectAccounting` split that Manage team uses. - - Build items (exactly three, in order): - - `createWorkspace`: `isComplete: true`, route `shouldUseNarrowLayout ? ROUTES.WORKSPACE_INITIAL.getRoute(activePolicyID, Navigation.getActiveRoute()) : ROUTES.WORKSPACE_OVERVIEW.getRoute(activePolicyID)` (matches Manage team pattern). - - `customizeCategories`: `isComplete: hasCustomCategories(policyCategories)`, route `ROUTES.WORKSPACE_CATEGORIES.getRoute(activePolicyID)`, with `isFeatureEnabled: policy.areCategoriesEnabled` and `enableFeature` calling `enablePolicyCategories` so tapping the row auto-enables categories (mirrors Manage team logic). - - `inviteAccountant`: `isComplete: Object.values(policy?.employeeList ?? {}).filter((member) => member?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length >= 2`, route `ROUTES.WORKSPACE_MEMBERS.getRoute(activePolicyID)`. - - Keep `MANAGE_TEAM` branch untouched. - - Optional refactor: extract a shared `useOnboardingIntent()` hook returning `introSelected?.choice ?? onboardingPurpose`, and use it in both this hook and `HomePage.tsx`. -2. Add translation key `inviteAccountant` to `homePage.gettingStartedSection` in EN and ES only: - - `[src/languages/en.ts](src/languages/en.ts)`: `inviteAccountant: 'Invite your accountant'`. - - `[src/languages/es.ts](src/languages/es.ts)`: generate the Spanish translation (JaimeGPT-style) and add it to the matching section. - - Do NOT edit other locale files in this PR. -3. Update `[src/pages/home/HomePage.tsx](src/pages/home/HomePage.tsx)` mobile layout: - - Read both `introSelected` (via `useOnyx(ONYXKEYS.NVP_INTRO_SELECTED)`) and `onboardingPurpose` (via `useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED)`), or use the shared `useOnboardingIntent()` helper if added. Compute `intent = introSelected?.choice ?? onboardingPurpose` so this stays in lockstep with `useGettingStartedItems`. - - In the narrow-layout branch, when `intent === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE`, render `ForYouSection` before `GettingStartedSection`. Otherwise preserve the existing order (`GettingStartedSection` then `ForYouSection`). - - Desktop right-column order is unchanged. - -### Phase 3 - Refactor - -- If the intent branching in `useGettingStartedItems.ts` grows, extract per-intent builders (e.g., `buildManageTeamItems`, `buildTrackWorkspaceItems`) for readability - no behavior change. -- Ensure `GettingStartedRow`'s existing `navigateToItem` handles the `createWorkspace`-always-checked row gracefully (current rendering hides the arrow icon when complete; confirm via snapshot/interaction test). - -### Phase 4 - Regression and Edge Cases - -- Custom category with `pendingAction === DELETE` does not count (already handled by `hasCustomCategories`). -- Default categories from `CONST.POLICY.DEFAULT_CATEGORIES` do not count. -- Member count uses `policy.employeeList` keys; verify owner-only policy returns 1 member and stays unchecked. -- Add tests for the 60-day boundary: day 60 inclusive shows, day 61 hides (matches `SIXTY_DAYS_MS`). -- Verify no regression to mobile ordering for non-Track intents by ensuring the default branch runs. - -## Decision Log (No Hidden Decisions) - -1. Decision: Mobile ordering swap is conditional on intent. - - Options considered: (a) conditional on Track, (b) universal swap, (c) no change. - - Chosen option: (a) conditional on Track. - - Source: User answer. - - Confirmation needed: No. -2. Decision: Workspace used for checks and navigation is `NVP_ACTIVE_POLICY_ID`. - - Options considered: active policy, first owned paid policy, onboarding policy ID. - - Chosen option: `NVP_ACTIVE_POLICY_ID`. - - Source: User answer. - - Confirmation needed: No. -3. Decision: Intent detection uses `TRACK_WORKSPACE` only. - - Options considered: `TRACK_WORKSPACE` only, or include `PERSONAL_SPEND`. - - Chosen option: `TRACK_WORKSPACE` only. - - Source: User answer. - - Confirmation needed: No. -4. Decision: Reuse `isWithinGettingStartedPeriod(firstDayFreeTrial)` (60-day window from trial start) for Track visibility. - - Options considered: New helper mirroring `shouldShowTrialEndedUI`, or reuse existing helper. - - Chosen option: Reuse existing helper - it already encodes "within 60 days from trial start" which covers both new sign-ups and active trials as specified in the issue. - - Source: Cross-validation of current `useGettingStartedItems` implementation; matches Manage team semantics. - - Confirmation needed: No. -5. Decision: Enforce `isPaidGroupPolicy` guard for Track (same guard stack as Manage team). - - Options considered: Enforce paid group policy, or allow any admin policy. - - Chosen option: Enforce paid group policy - consistent with Manage team, avoids personal-policy edge cases where `areCategoriesEnabled`/`enablePolicyCategories` do not apply. The Track workspace auto-created by `autoCreateTrackWorkspace` is a group policy so this does not exclude valid Track users. - - Source: Cross-validation. - - Confirmation needed: No. -6. Decision: `Create a workspace` row remains visually always-checked, navigating to the active workspace overview (matches Manage team behavior). - - Options considered: Hide the row entirely, or always checked. - - Chosen option: Always checked (issue explicitly calls this out). - - Source: Issue. - - Confirmation needed: No. -7. Decision: Intent source in `HomePage.tsx` mobile ordering matches `useGettingStartedItems` - `introSelected?.choice ?? onboardingPurpose`. - - Options considered: Use `introSelected?.choice` only, or mirror the hook's fallback. - - Chosen option: Mirror the hook's fallback to avoid transient mismatch where items render as Track but section order does not swap. - - Source: Cross-validation. - - Confirmation needed: No. - -## Validation Checklist - -- Every plan step maps to an explicit requirement or confirmed decision. -- Tests are listed before implementation tasks (TDD). -- All decisions (1-7) are finalized; no pending confirmations. -- Open questions were raised and answered before finalizing. -- No changes to Manage team behavior or desktop layout. -- EN and ES locale files updated with the new `inviteAccountant` key (CI will fail otherwise). Other locales intentionally left for a follow-up. - -## Files Touched - -- [src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts](src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts) -- [src/pages/home/HomePage.tsx](src/pages/home/HomePage.tsx) -- [src/languages/en.ts](src/languages/en.ts) -- [src/languages/es.ts](src/languages/es.ts) -- [tests/unit/hooks/useGettingStartedItems.test.ts](tests/unit/hooks/useGettingStartedItems.test.ts) -- (optional) new [tests/unit/pages/HomePage.test.tsx](tests/unit/pages/HomePage.test.tsx) for mobile ordering (extend an existing home-page test file if present) -- (optional) new shared hook `src/hooks/useOnboardingIntent.ts` if the intent-resolution refactor is taken - From 92090ba0f9e455e029e3904dc9503b476ae678d6 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Mon, 27 Apr 2026 12:49:11 +0200 Subject: [PATCH 20/23] Fix duplicate key and lint issues in locked bank account tests --- .../useTimeSensitiveLockedBankAccount.test.ts | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts diff --git a/tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts b/tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts new file mode 100644 index 000000000000..267033e63043 --- /dev/null +++ b/tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts @@ -0,0 +1,305 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import {renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import useTimeSensitiveLockedBankAccount from '@pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {BankAccount, BankAccountList} from '@src/types/onyx'; +import type Policy from '@src/types/onyx/Policy'; +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +const PRIMARY_LOGIN = 'user@example.com'; +const OTHER_LOGIN = 'other@example.com'; + +function makePolicy(overrides: Partial & {id: string}): Policy { + return { + ...overrides, + name: overrides.name ?? `Policy ${overrides.id}`, + } as Policy; +} + +function makeBankAccount(bankAccountID: number, state: string, type?: string): BankAccount { + return { + bankCurrency: 'USD', + bankCountry: 'US', + accountData: { + bankAccountID, + state, + type, + }, + } as BankAccount; +} + +describe('useTimeSensitiveLockedBankAccount', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('returns empty array when adminPolicies is undefined and BANK_ACCOUNT_LIST is empty', () => { + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount(undefined)); + + expect(result.current.lockedBankAccounts).toEqual([]); + }); + + it('returns empty array when adminPolicies is an empty array and BANK_ACCOUNT_LIST is empty', () => { + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([])); + + expect(result.current.lockedBankAccounts).toEqual([]); + }); + + it('returns a workspace entry when the policy has a locked achAccount and current user is the reimburser', async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: PRIMARY_LOGIN}); + await waitForBatchedUpdates(); + + const policy = makePolicy({ + id: 'policy1', + name: 'Acme Corp', + achAccount: { + bankAccountID: 100, + accountNumber: '****1234', + routingNumber: '123456789', + addressName: 'Test Account', + bankName: 'Test Bank', + reimburser: PRIMARY_LOGIN, + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([policy])); + + expect(result.current.lockedBankAccounts).toHaveLength(1); + expect(result.current.lockedBankAccounts.at(0)).toMatchObject({ + bankAccountID: 100, + policyName: 'Acme Corp', + }); + }); + + it('skips the workspace entry when the current user is NOT the reimburser', async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: PRIMARY_LOGIN}); + await waitForBatchedUpdates(); + + const policy = makePolicy({ + id: 'policy1', + achAccount: { + bankAccountID: 100, + accountNumber: '****1234', + routingNumber: '123456789', + addressName: 'Test Account', + bankName: 'Test Bank', + reimburser: OTHER_LOGIN, + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([policy])); + + expect(result.current.lockedBankAccounts).toHaveLength(0); + }); + + it('renders a personal entry when BANK_ACCOUNT_LIST has a locked PERSONAL account', async () => { + const bankAccountList: BankAccountList = { + '200': makeBankAccount(200, CONST.BANK_ACCOUNT.STATE.LOCKED, CONST.BANK_ACCOUNT.TYPE.PERSONAL), + }; + + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([])); + + const entry = result.current.lockedBankAccounts.at(0); + expect(result.current.lockedBankAccounts).toHaveLength(1); + expect(entry?.bankAccountID).toBe(200); + expect(entry?.policyName).toBeUndefined(); + }); + + it('renders a personal entry when accountData.type is undefined (treated as personal)', async () => { + const bankAccountList: BankAccountList = { + '201': makeBankAccount(201, CONST.BANK_ACCOUNT.STATE.LOCKED, undefined), + }; + + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([])); + + expect(result.current.lockedBankAccounts).toHaveLength(1); + expect(result.current.lockedBankAccounts.at(0)?.bankAccountID).toBe(201); + }); + + it('skips a BANK_ACCOUNT_LIST entry whose accountData.type is BUSINESS', async () => { + const bankAccountList: BankAccountList = { + '300': makeBankAccount(300, CONST.BANK_ACCOUNT.STATE.LOCKED, CONST.BANK_ACCOUNT.TYPE.BUSINESS), + }; + + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([])); + + expect(result.current.lockedBankAccounts).toHaveLength(0); + }); + + it('suppresses the personal widget when the same bankAccountID is also a workspace locked account', async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: PRIMARY_LOGIN}); + + const bankAccountList: BankAccountList = { + '100': makeBankAccount(100, CONST.BANK_ACCOUNT.STATE.LOCKED, CONST.BANK_ACCOUNT.TYPE.PERSONAL), + }; + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); + + const policy = makePolicy({ + id: 'policy1', + achAccount: { + bankAccountID: 100, + accountNumber: '****1234', + routingNumber: '123456789', + addressName: 'Test Account', + bankName: 'Test Bank', + reimburser: PRIMARY_LOGIN, + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([policy])); + + // Only one entry — the workspace one; personal is deduped + expect(result.current.lockedBankAccounts).toHaveLength(1); + expect(result.current.lockedBankAccounts.at(0)?.policyName).toBeDefined(); + }); + + it('adds bankAccountID to the dedupe Set even when non-reimburser admin — personal widget is suppressed', async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: PRIMARY_LOGIN}); + + const bankAccountList: BankAccountList = { + '100': makeBankAccount(100, CONST.BANK_ACCOUNT.STATE.LOCKED, CONST.BANK_ACCOUNT.TYPE.PERSONAL), + }; + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); + + const policy = makePolicy({ + id: 'policy1', + achAccount: { + bankAccountID: 100, + accountNumber: '****1234', + routingNumber: '123456789', + addressName: 'Test Account', + bankName: 'Test Bank', + reimburser: OTHER_LOGIN, + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([policy])); + + // Current behavior: dedupe Set is populated before reimburser check, + // so neither workspace nor personal widget renders. + // This test pins the current behavior so a fix shows up as an intentional change. + expect(result.current.lockedBankAccounts).toHaveLength(0); + }); + + it('handles null/undefined BANK_ACCOUNT_LIST without throwing', async () => { + // Do not merge any bank account list — Onyx will return undefined/null + await waitForBatchedUpdates(); + + expect(() => { + renderHook(() => useTimeSensitiveLockedBankAccount([])); + }).not.toThrow(); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([])); + expect(result.current.lockedBankAccounts).toEqual([]); + }); + + it('handles nullish entries inside BANK_ACCOUNT_LIST without throwing', async () => { + // Simulate a partially cleared Onyx collection where an entry is null + const bankAccountList = {'999': null} as unknown as BankAccountList; + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); + + expect(() => { + renderHook(() => useTimeSensitiveLockedBankAccount([])); + }).not.toThrow(); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([])); + expect(result.current.lockedBankAccounts).toEqual([]); + }); + + it('emits keys in workspace-policyID-bankAccountID format for workspace entries', async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: PRIMARY_LOGIN}); + await waitForBatchedUpdates(); + + const policy = makePolicy({ + id: 'policy42', + achAccount: { + bankAccountID: 777, + accountNumber: '****5678', + routingNumber: '987654321', + addressName: 'Test Account', + bankName: 'Test Bank', + reimburser: PRIMARY_LOGIN, + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([policy])); + + expect(result.current.lockedBankAccounts.at(0)?.key).toBe('workspace-policy42-777'); + }); + + it('emits keys in personal-bankAccountID format for personal entries', async () => { + const bankAccountList: BankAccountList = { + '888': makeBankAccount(888, CONST.BANK_ACCOUNT.STATE.LOCKED, CONST.BANK_ACCOUNT.TYPE.PERSONAL), + }; + + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([])); + + expect(result.current.lockedBankAccounts.at(0)?.key).toBe('personal-888'); + }); + + it('does not render a workspace entry when achAccount state is not LOCKED', async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: PRIMARY_LOGIN}); + await waitForBatchedUpdates(); + + const policy = makePolicy({ + id: 'policy1', + achAccount: { + bankAccountID: 100, + accountNumber: '****1234', + routingNumber: '123456789', + addressName: 'Test Account', + bankName: 'Test Bank', + reimburser: PRIMARY_LOGIN, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + }, + }); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([policy])); + + expect(result.current.lockedBankAccounts).toHaveLength(0); + }); + + it('does not render a personal entry when the account state is not LOCKED', async () => { + const bankAccountList: BankAccountList = { + '200': makeBankAccount(200, CONST.BANK_ACCOUNT.STATE.OPEN, CONST.BANK_ACCOUNT.TYPE.PERSONAL), + }; + + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([])); + + expect(result.current.lockedBankAccounts).toHaveLength(0); + }); +}); From 9306abde3e5a8ea164d1ef85e2fc974d879ffc02 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Mon, 27 Apr 2026 13:40:45 +0200 Subject: [PATCH 21/23] Fix workspaceLockedBankAccountIDs ordering to allow personal widget for non-reimburser admins --- .../hooks/useTimeSensitiveLockedBankAccount.ts | 2 +- .../hooks/useTimeSensitiveLockedBankAccount.test.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts index f4bb9567b931..9520d7f4e1c3 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveLockedBankAccount.ts @@ -28,12 +28,12 @@ function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) continue; } - workspaceLockedBankAccountIDs.add(achAccount.bankAccountID); const isCurrentUserReimburser = !!primaryLogin && achAccount.reimburser === primaryLogin; if (!isCurrentUserReimburser) { continue; } + workspaceLockedBankAccountIDs.add(achAccount.bankAccountID); lockedBankAccounts.push({ key: `workspace-${policy.id}-${achAccount.bankAccountID}`, bankAccountID: achAccount.bankAccountID, diff --git a/tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts b/tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts index 267033e63043..48c2cb1f24b2 100644 --- a/tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts +++ b/tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts @@ -177,7 +177,7 @@ describe('useTimeSensitiveLockedBankAccount', () => { expect(result.current.lockedBankAccounts.at(0)?.policyName).toBeDefined(); }); - it('adds bankAccountID to the dedupe Set even when non-reimburser admin — personal widget is suppressed', async () => { + it('shows the personal widget when a non-reimburser admin has the same bankAccountID as a workspace account', async () => { await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: PRIMARY_LOGIN}); const bankAccountList: BankAccountList = { @@ -201,10 +201,11 @@ describe('useTimeSensitiveLockedBankAccount', () => { const {result} = renderHook(() => useTimeSensitiveLockedBankAccount([policy])); - // Current behavior: dedupe Set is populated before reimburser check, - // so neither workspace nor personal widget renders. - // This test pins the current behavior so a fix shows up as an intentional change. - expect(result.current.lockedBankAccounts).toHaveLength(0); + // Option A fix: workspaceLockedBankAccountIDs.add(...) only runs after the reimburser check, + // so no workspace widget is shown but the personal widget is NOT suppressed. + expect(result.current.lockedBankAccounts).toHaveLength(1); + expect(result.current.lockedBankAccounts.at(0)?.policyName).toBeUndefined(); + expect(result.current.lockedBankAccounts.at(0)?.key).toBe('personal-100'); }); it('handles null/undefined BANK_ACCOUNT_LIST without throwing', async () => { From 83ca9a56ad586fc18dc11f6271975aa35e9f0f72 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Mon, 4 May 2026 11:52:46 +0200 Subject: [PATCH 22/23] Pass delegateAccountID to pressLockedBankAccount in UnlockBankAccount --- .../home/TimeSensitiveSection/items/UnlockBankAccount.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx b/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx index dd89613714b8..dab09ce2d404 100644 --- a/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx +++ b/src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx @@ -2,6 +2,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import React from 'react'; import BaseWidgetItem from '@components/BaseWidgetItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDelegateAccountID from '@hooks/useDelegateAccountID'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -26,6 +27,7 @@ function UnlockBankAccount({bankAccountID, policyName}: UnlockBankAccountProps) const [betas] = useOnyx(ONYXKEYS.BETAS); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const delegateAccountID = useDelegateAccountID(); const title = policyName ? translate('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle') : translate('homePage.timeSensitiveSection.unlockBankAccount.personalTitle'); @@ -34,7 +36,7 @@ function UnlockBankAccount({bankAccountID, policyName}: UnlockBankAccountProps) : translate('homePage.timeSensitiveSection.unlockBankAccount.personalSubtitle'); const handleCtaPress = () => { - pressLockedBankAccount(bankAccountID, translate, conciergeReportID); + pressLockedBankAccount(bankAccountID, translate, conciergeReportID, delegateAccountID); navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas); }; From 1dd9cb493d012d92b5ce86a7100ee176b8a6d690 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Mon, 4 May 2026 12:13:41 +0200 Subject: [PATCH 23/23] Fix UnlockBankAccount test assertion to include delegateAccountID argument --- .../pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx index fcdf19273aeb..631ec94a5743 100644 --- a/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx +++ b/tests/unit/pages/home/TimeSensitiveSection/UnlockBankAccountTest.tsx @@ -311,7 +311,7 @@ describe('TimeSensitiveSection - UnlockBankAccount', () => { const cta = screen.getByText('homePage.timeSensitiveSection.ctaFix'); fireEvent.press(cta); - expect(pressLockedBankAccount).toHaveBeenCalledWith(LOCKED_BANK_ACCOUNT_ID, expect.any(Function), CONCIERGE_REPORT_ID); + expect(pressLockedBankAccount).toHaveBeenCalledWith(LOCKED_BANK_ACCOUNT_ID, expect.any(Function), CONCIERGE_REPORT_ID, undefined); expect(navigateToConciergeChat).toHaveBeenCalledWith(CONCIERGE_REPORT_ID, undefined, ADMIN_ACCOUNT_ID, false, undefined); }); });