Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,15 @@ const DYNAMIC_ROUTES = {
SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
SCREENS.SEARCH.ROOT,
],
// Entry points that already know the user is adding a personal deposit account (e.g. a queued reimbursement) pass true so the
// bank account purpose screen isn't shown after validation.
getRoute: (shouldSkipPurposeSelection?: boolean) => `add-bank-account/verify-account${shouldSkipPurposeSelection ? '?shouldSkipPurposeSelection=true' : ''}` as const,
queryParams: ['shouldSkipPurposeSelection'],
// Entry points that already know the user is adding a personal deposit account (e.g. a queued reimbursement).
getRoute: (shouldSkipPurposeSelection?: boolean, shouldSetUpUSBankAccount?: boolean) =>
getUrlWithParams('add-bank-account/verify-account', {
// If true the bank account purpose screen isn't shown after validation
shouldSkipPurposeSelection: shouldSkipPurposeSelection ? 'true' : undefined,
// If true US bank account setup flow must be started after validation
shouldSetUpUSBankAccount: shouldSetUpUSBankAccount ? 'true' : undefined,
}),
queryParams: ['shouldSkipPurposeSelection', 'shouldSetUpUSBankAccount'],
},
BANK_ACCOUNT_VERIFY_ACCOUNT: {
path: 'verify-bank-account',
Expand Down
7 changes: 5 additions & 2 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,11 @@ type SettingsNavigatorParamList = {
};
[SCREENS.SETTINGS.DYNAMIC_ADD_BANK_ACCOUNT_VERIFY_ACCOUNT]:
| {
/** Whether the bank account purpose screen should be skipped after the account is validated */
shouldSkipPurposeSelection?: boolean;
/** Whether the bank account purpose screen should be skipped after the account is validated. */
shouldSkipPurposeSelection?: 'true';

/** Whether the US bank account flow should open after the account is validated. */
shouldSetUpUSBankAccount?: 'true';
}
| undefined;
[SCREENS.SETTINGS.DYNAMIC_EXIT_SURVEY_REASON]: undefined;
Expand Down
7 changes: 4 additions & 3 deletions src/libs/Url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function getSearchParamFromPath(path: string, param: string) {
return getParamFromQueryString(backToQueryString);
}

type UrlWithParams<TBase extends string> = `${TBase}${'' | `?${string}` | `&${string}`}`;
type UrlWithParams<TBase extends string> = TBase | `${TBase}${`?${string}` | `&${string}`}`;
type UrlParams = {backTo?: string; forwardTo?: string} & Record<string, string | number | undefined>;
/**
* Generate a URL with properly encoded query parameters.
Expand All @@ -139,7 +139,8 @@ type UrlParams = {backTo?: string; forwardTo?: string} & Record<string, string |
* @param params - Object containing key-value pairs for query parameters.
* @returns A URL string with encoded query parameters.
*/
function getUrlWithParams<TBase extends string, TParams extends UrlParams>(baseUrl: TBase, params: TParams): UrlWithParams<TBase> {
function getUrlWithParams<TBase extends string, TParams extends UrlParams>(baseUrl: TBase, params: TParams): UrlWithParams<TBase>;
function getUrlWithParams(baseUrl: string, params: UrlParams): string {
const [path, existingQuery] = baseUrl.split('?', 2);
const searchParams = new URLSearchParams(existingQuery || '');

Expand All @@ -150,7 +151,7 @@ function getUrlWithParams<TBase extends string, TParams extends UrlParams>(baseU
}

const queryString = searchParams.toString();
return (queryString ? `${path}?${queryString}` : path) as UrlWithParams<TBase>;
return queryString ? `${path}?${queryString}` : path;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/BankAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ function openPersonalBankAccountSetupView({

if (!isUserValidated) {
// This flow always adds a personal deposit account, so the purpose screen is skipped once the account is validated.
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.ADD_BANK_ACCOUNT_VERIFY_ACCOUNT.getRoute(true)));
Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.ADD_BANK_ACCOUNT_VERIFY_ACCOUNT.getRoute(true, shouldSetUpUSBankAccount)));
return;
}
if (shouldSetUpUSBankAccount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@ import React, {useMemo} from 'react';
type DynamicAddBankAccountVerifyAccountPageProps = PlatformStackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.DYNAMIC_ADD_BANK_ACCOUNT_VERIFY_ACCOUNT>;

function DynamicAddBankAccountVerifyAccountPage({route}: DynamicAddBankAccountVerifyAccountPageProps) {
const {shouldSkipPurposeSelection} = route.params ?? {};
const {shouldSkipPurposeSelection, shouldSetUpUSBankAccount} = route.params ?? {};
const backPath = useDynamicBackPath(DYNAMIC_ROUTES.ADD_BANK_ACCOUNT_VERIFY_ACCOUNT.path);
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const currentUserEmail = getCurrentUserEmail();
const isAdmin = useMemo(() => hasActiveAdminWorkspaces(currentUserEmail ?? '', allPolicies), [currentUserEmail, allPolicies]);
const navigateForwardTo = isAdmin && !shouldSkipPurposeSelection ? ROUTES.SETTINGS_BANK_ACCOUNT_PURPOSE : ROUTES.SETTINGS_ADD_BANK_ACCOUNT.getRoute(backPath);
// This forward path must agree with the validated branch of openPersonalBankAccountSetupView.
let navigateForwardTo;
if (shouldSetUpUSBankAccount === 'true') {
navigateForwardTo = ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT.getRoute();
} else if (isAdmin && shouldSkipPurposeSelection !== 'true') {
navigateForwardTo = ROUTES.SETTINGS_BANK_ACCOUNT_PURPOSE;
} else {
navigateForwardTo = ROUTES.SETTINGS_ADD_BANK_ACCOUNT.getRoute(backPath);
}

return (
<VerifyAccountPageBase
Expand Down
11 changes: 10 additions & 1 deletion tests/actions/BankAccountsTest.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {clearPersonalBankAccount, connectBankAccountWithPlaid, openPersonalBankAccountSetupView} from '@libs/actions/BankAccounts';
import {WRITE_COMMANDS} from '@libs/API/types';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import Navigation from '@libs/Navigation/Navigation';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES';
import type {ReimbursementAccountForm} from '@src/types/form/ReimbursementAccountForm';
import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount';

Expand Down Expand Up @@ -174,6 +175,14 @@ describe('actions/BankAccounts', () => {

expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT.getRoute());
});

test('carries shouldSetUpUSBankAccount to the verify account page when the user is not validated', async () => {
openPersonalBankAccountSetupView({shouldSetUpUSBankAccount: true, isUserValidated: false});
await waitForBatchedUpdates();

expect(Navigation.navigate).toHaveBeenCalledWith(createDynamicRoute(DYNAMIC_ROUTES.ADD_BANK_ACCOUNT_VERIFY_ACCOUNT.getRoute(true, true)));
expect(Navigation.navigate).toHaveBeenCalledWith(expect.stringContaining('shouldSetUpUSBankAccount=true'));
});
});

describe('clearPersonalBankAccount', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {render} from '@testing-library/react-native';

import OnyxListItemProvider from '@components/OnyxListItemProvider';

import {navigationRef} from '@libs/Navigation/Navigation';
import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import {hasActiveAdminWorkspaces} from '@libs/PolicyUtils';

import DynamicAddBankAccountVerifyAccountPage from '@pages/settings/Wallet/DynamicAddBankAccountVerifyAccountPage';

import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';

import {NavigationContainer} from '@react-navigation/native';
import React from 'react';
import Onyx from 'react-native-onyx';

import waitForBatchedUpdates from '../../../../utils/waitForBatchedUpdates';

const mockVerifyAccountPageBase = jest.fn<null, [Record<string, unknown>]>(() => null);

jest.mock('@pages/settings/VerifyAccountPageBase', () => ({
__esModule: true,
default: (props: Record<string, unknown>) => mockVerifyAccountPageBase(props),
}));

jest.mock('@hooks/useDynamicBackPath', () => jest.fn(() => 'settings/wallet'));

jest.mock('@libs/PolicyUtils', () => ({
...jest.requireActual<Record<string, unknown>>('@libs/PolicyUtils'),
hasActiveAdminWorkspaces: jest.fn(),
}));

const mockedHasActiveAdminWorkspaces = jest.mocked(hasActiveAdminWorkspaces);

const Stack = createPlatformStackNavigator<SettingsNavigatorParamList>();

function renderPage(params: SettingsNavigatorParamList[typeof SCREENS.SETTINGS.DYNAMIC_ADD_BANK_ACCOUNT_VERIFY_ACCOUNT]) {
return render(
<OnyxListItemProvider>
<NavigationContainer ref={navigationRef}>
<Stack.Navigator initialRouteName={SCREENS.SETTINGS.DYNAMIC_ADD_BANK_ACCOUNT_VERIFY_ACCOUNT}>
<Stack.Screen
name={SCREENS.SETTINGS.DYNAMIC_ADD_BANK_ACCOUNT_VERIFY_ACCOUNT}
component={DynamicAddBankAccountVerifyAccountPage}
initialParams={params}
/>
</Stack.Navigator>
</NavigationContainer>
</OnyxListItemProvider>,
);
}

describe('DynamicAddBankAccountVerifyAccountPage', () => {
beforeAll(() => {
Onyx.init({keys: ONYXKEYS});
});

beforeEach(() => {
mockVerifyAccountPageBase.mockClear();
return Onyx.clear().then(waitForBatchedUpdates);
});

// The forward path must mirror the validated branch of openPersonalBankAccountSetupView: shouldSetUpUSBankAccount wins over
// the purpose screen, so a user who validates here lands in the same flow a validated user is sent to directly.
it.each([
{
name: 'US bank account flow when shouldSetUpUSBankAccount is set (admin)',
params: {shouldSetUpUSBankAccount: 'true'} as const,
isAdmin: true,
expected: ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT.getRoute(),
},
{
name: 'US bank account flow when shouldSetUpUSBankAccount is set (non-admin)',
params: {shouldSetUpUSBankAccount: 'true'} as const,
isAdmin: false,
expected: ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT.getRoute(),
},
{name: 'purpose screen for admins without any flags', params: undefined, isAdmin: true, expected: ROUTES.SETTINGS_BANK_ACCOUNT_PURPOSE},
{name: 'add bank account for non-admins without any flags', params: undefined, isAdmin: false, expected: ROUTES.SETTINGS_ADD_BANK_ACCOUNT.getRoute('settings/wallet')},
{
name: 'add bank account for admins when shouldSkipPurposeSelection is set',
params: {shouldSkipPurposeSelection: 'true'} as const,
isAdmin: true,
expected: ROUTES.SETTINGS_ADD_BANK_ACCOUNT.getRoute('settings/wallet'),
},
])('forwards to the $name', async ({params, isAdmin, expected}) => {
mockedHasActiveAdminWorkspaces.mockReturnValue(isAdmin);

renderPage(params);
await waitForBatchedUpdates();

expect(mockVerifyAccountPageBase).toHaveBeenCalledWith(expect.objectContaining({navigateForwardTo: expected}));
});
});
Loading