From 278972a2774012c87155a1b03fccd145fba72ee2 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Thu, 17 Sep 2026 16:39:47 +0000 Subject: [PATCH 1/2] Allow the workflows bank account row to be tapped while offline The unlock request on a locked bank account is optimistic and queues until reconnect, so the offline gate on the row blocked a flow that works offline everywhere else. This mirrors Settings > Wallet, which has no such gate. Co-authored-by: Pujan Shah --- .../workflows/tabs/WorkflowsPaymentsTab.tsx | 4 +- ...orkspaceWorkflowsLockedBankAccountTest.tsx | 183 ++++++++++++++++++ 2 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx diff --git a/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx b/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx index 488712f6f637..ae2848b12ee9 100644 --- a/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx +++ b/src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx @@ -160,7 +160,7 @@ function WorkflowsPaymentsTab({policyID}: WorkflowsPaymentsTabProps) { ) : undefined; const bankConnectionMessage = bankConnectionStatus?.messageKey ? translate(bankConnectionStatus.messageKey) : undefined; const bankConnectionActionText = bankConnectionStatus?.actionKey ? translate(bankConnectionStatus.actionKey) : undefined; - const canInteractWithBankAccountRow = canWritePayments && !isOffline && !isBankAccountPendingDelete; + const canInteractWithBankAccountRow = canWritePayments && !isBankAccountPendingDelete; // Only the reimburser can send the unlock request, so a locked account offers no action to anyone else rather than // an Unlock button that would instead start connecting a different bank account. const canPerformBankAccountAction = !isBusinessBankAccountLocked || isUserReimburser; @@ -209,7 +209,7 @@ function WorkflowsPaymentsTab({policyID}: WorkflowsPaymentsTabProps) { descriptionTextStyle: isBankAccountPendingDelete ? styles.offlineFeedbackDeleted : undefined, sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.BANK_ACCOUNT, shouldGreyOutWhenDisabled: !policy?.pendingFields?.reimbursementChoice, - disabled: isOffline || !canWritePayments || isBankAccountPendingDelete, + disabled: !canWritePayments || isBankAccountPendingDelete, shouldShowRightIcon: canWritePayments && !isBankAccountPendingDelete, interactive: canWritePayments && !isBankAccountPendingDelete, descriptionAddon: bankConnectionStatusAddon, diff --git a/tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx b/tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx new file mode 100644 index 000000000000..1f0f453d0b69 --- /dev/null +++ b/tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx @@ -0,0 +1,183 @@ +import {act, fireEvent, render, screen} from '@testing-library/react-native'; + +import ComposeProviders from '@components/ComposeProviders'; +import {CurrentUserPersonalDetailsProvider} from '@components/CurrentUserPersonalDetailsProvider'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import {ModalProvider} from '@components/Modal/Global/ModalContext'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; +import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout'; +import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; + +import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; +import {setForceOffline} from '@libs/NetworkState'; + +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; + +import WorkspaceWorkflowsPageRevamp from '@pages/workspace/workflows/WorkspaceWorkflowsPageRevamp'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import type {Policy} from '@src/types/onyx'; + +import {PortalProvider} from '@gorhom/portal'; +import {NavigationContainer} from '@react-navigation/native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import type * as ReportUserActionsModule from '@userActions/Report'; + +import type * as MockReanimatedModalModule from '../utils/mockReanimatedModal'; + +import createMock from '../utils/createMock'; +import * as LHNTestUtils from '../utils/LHNTestUtils'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +jest.mock('@src/components/ConfirmedRoute.tsx'); + +jest.mock('react-native-render-html', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const {View: MockView} = require('react-native'); + return { + RenderHTMLConfigProvider: ({children}: {children: React.ReactNode}) => children, + RenderHTMLSource: () => , + }; +}); + +jest.mock('@components/Modal/ReanimatedModal', () => { + const {default: MockReanimatedModal} = jest.requireActual('../utils/mockReanimatedModal'); + return MockReanimatedModal; +}); + +const mockNavigateToConciergeChat = jest.fn(); +jest.mock('@userActions/Report', () => ({ + ...jest.requireActual('@userActions/Report'), + navigateToConciergeChat: () => { + mockNavigateToConciergeChat(); + }, +})); + +TestHelper.setupGlobalFetchMock(); + +const POLICY_ID = 'workflows-locked-bank-account-test'; +const CURRENT_USER_LOGIN = 'test@user.com'; +const OTHER_REIMBURSER_LOGIN = 'reimburser@user.com'; +const BANK_ACCOUNT_ID = 123456; + +type TestNavigatorParamList = WorkspaceSplitNavigatorParamList; + +const Stack = createPlatformStackNavigator(); + +const buildPolicyWithLockedBankAccount = (reimburser: string): Policy => + ({ + ...LHNTestUtils.getFakePolicy(POLICY_ID), + type: CONST.POLICY.TYPE.CORPORATE, + role: CONST.POLICY.ROLE.ADMIN, + outputCurrency: 'USD', + areWorkflowsEnabled: true, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + achAccount: { + reimburser, + bankAccountID: BANK_ACCOUNT_ID, + accountNumber: '1234567890', + routingNumber: '011000015', + bankName: 'Test Bank', + addressName: 'Test Address', + state: CONST.BANK_ACCOUNT.STATE.LOCKED, + }, + }) as Policy; + +const renderPage = () => + render( + + + + + + + + + + + , + ); + +const setUpLockedBankAccount = async (reimburser: string) => { + await TestHelper.signInWithTestUser(1, CURRENT_USER_LOGIN); + await act(async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: CURRENT_USER_LOGIN}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicyWithLockedBankAccount(reimburser)); + }); +}; + +const getInitiatingBankAccountUnlock = () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.INITIATING_BANK_ACCOUNT_UNLOCK, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); + +describe('WorkspaceWorkflowsPageRevamp - locked bank account row', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await act(async () => { + await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.EN); + }); + jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue( + createMock({ + isSmallScreenWidth: false, + shouldUseNarrowLayout: false, + }), + ); + }); + + afterEach(async () => { + setForceOffline(false); + mockNavigateToConciergeChat.mockClear(); + await act(async () => { + await Onyx.clear(); + }); + jest.restoreAllMocks(); + }); + + it('lets the reimburser request an unlock while offline, because the request is optimistic and queues until reconnect', async () => { + await setUpLockedBankAccount(CURRENT_USER_LOGIN); + setForceOffline(true); + + renderPage(); + await waitForBatchedUpdatesWithAct(); + + fireEvent.press(screen.getByText(TestHelper.translateLocal('walletPage.bankAccountStatus.unlock'))); + await waitForBatchedUpdatesWithAct(); + + expect(mockNavigateToConciergeChat).toHaveBeenCalled(); + await expect(getInitiatingBankAccountUnlock()).resolves.toEqual(expect.objectContaining({bankAccountIDToUnlock: BANK_ACCOUNT_ID})); + }); + + it('does not offer the unlock action offline to someone who is not the reimburser', async () => { + await setUpLockedBankAccount(OTHER_REIMBURSER_LOGIN); + setForceOffline(true); + + renderPage(); + await waitForBatchedUpdatesWithAct(); + + // Only the reimburser can send the unlock request, so nobody else gets an Unlock button to press. + expect(screen.queryByText(TestHelper.translateLocal('walletPage.bankAccountStatus.unlock'))).not.toBeOnTheScreen(); + await expect(getInitiatingBankAccountUnlock()).resolves.toBeUndefined(); + }); +}); From 6ab9c99d951a2ac9cf78b08991409173d7ceb389 Mon Sep 17 00:00:00 2001 From: "Pujan Shah (via MelvinBot)" Date: Thu, 17 Sep 2026 16:54:48 +0000 Subject: [PATCH 2/2] Apply oxfmt import ordering to WorkspaceWorkflowsLockedBankAccountTest Co-authored-by: Pujan Shah --- tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx b/tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx index 1f0f453d0b69..5430df73fab3 100644 --- a/tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx +++ b/tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx @@ -17,6 +17,8 @@ import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import WorkspaceWorkflowsPageRevamp from '@pages/workspace/workflows/WorkspaceWorkflowsPageRevamp'; +import type * as ReportUserActionsModule from '@userActions/Report'; + import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; @@ -27,8 +29,6 @@ import {NavigationContainer} from '@react-navigation/native'; import React from 'react'; import Onyx from 'react-native-onyx'; -import type * as ReportUserActionsModule from '@userActions/Report'; - import type * as MockReanimatedModalModule from '../utils/mockReanimatedModal'; import createMock from '../utils/createMock';