Skip to content
Merged
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
58 changes: 58 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,65 @@ const CONST = {
VALIDATION: 'ValidationStep',
ENABLE: 'EnableStep',
},
PAGE_NAMES: {
COUNTRY: 'currency-and-country',
BANK_ACCOUNT: 'bank-info',
REQUESTOR: 'personal-info',
VERIFY_IDENTITY: 'verify-identity',
COMPANY: 'company',
BENEFICIAL_OWNERS: 'business-owner',
ACH_CONTRACT: 'ach-contract',
VALIDATION: 'validation',
ENABLE: 'enable',
},
STEP_NAMES: ['1', '2', '3', '4', '5', '6'],
BANK_INFO_STEP: {
SUB_PAGE_NAMES: {
MANUAL: 'manual',
PLAID: 'plaid',
},
},
PERSONAL_INFO_STEP: {
SUB_PAGE_NAMES: {
FULL_NAME: 'full-name',
DATE_OF_BIRTH: 'date-of-birth',
SSN: 'ssn',
ADDRESS: 'address',
CONFIRMATION: 'confirmation',
},
},
BUSINESS_INFO_STEP: {
SUB_PAGE_NAMES: {
NAME: 'name',
TAX_ID: 'tax-id',
WEBSITE: 'website',
PHONE: 'phone',
ADDRESS: 'address',
TYPE: 'type',
INCORPORATION_DATE: 'start-date',
INCORPORATION_STATE: 'state',
INCORPORATION_CODE: 'code',
CONFIRMATION: 'confirmation',
},
},
BENEFICIAL_OWNERS_STEP: {
SUB_PAGE_NAMES: {
IS_USER_UBO: 'is-user-ubo',
IS_ANYONE_ELSE_UBO: 'is-anyone-else-ubo',
ARE_THERE_MORE_UBOS: 'are-there-more-ubos',
UBOS_LIST: 'ubos-list',
LEGAL_NAME: 'legal-name',
DATE_OF_BIRTH: 'date-of-birth',
SSN: 'ssn',
ADDRESS: 'address',
CONFIRMATION: 'confirmation',
},
},
COMPLETE_VERIFICATION_STEP: {
SUB_PAGE_NAMES: {
CONFIRM_AGREEMENTS: 'confirm-agreements',
},
},
SUBSTEP: {
MANUAL: 'manual',
PLAID: 'plaid',
Expand Down
43 changes: 20 additions & 23 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {IOUAction, IOURequestType, IOUType, OdometerImageType} from './CONS
import type {ReplacementReason} from './libs/actions/Card';
import Log from './libs/Log';
import type {RootNavigatorParamList} from './libs/Navigation/types';
import type {ReimbursementAccountStepToOpen} from './libs/ReimbursementAccountUtils';
import StringUtils from './libs/StringUtils';
import {getUrlWithParams} from './libs/Url';
import SCREENS from './SCREENS';
Expand Down Expand Up @@ -864,34 +863,20 @@ const ROUTES = {

getRoute: (policyID?: string, backTo?: string) => getUrlWithBackToParam(`bank-account/${VERIFY_ACCOUNT}?policyID=${policyID}`, backTo),
},
BANK_ACCOUNT_NEW: 'bank-account/new',
BANK_ACCOUNT_PERSONAL: 'bank-account/personal',
// TODO: rename the route as no longer accepts step
BANK_ACCOUNT_WITH_STEP_TO_OPEN: {
route: 'bank-account/:stepToOpen?',
getRoute: ({
policyID,
stepToOpen = '',
bankAccountID,
backTo,
subStepToOpen,
}: {
policyID: string | undefined;
stepToOpen?: ReimbursementAccountStepToOpen;
bankAccountID?: number;
backTo?: string;
subStepToOpen?: typeof CONST.BANK_ACCOUNT.STEP.COUNTRY;
}) => {
if (!policyID && !bankAccountID) {
return getUrlWithBackToParam(`bank-account/${stepToOpen}`, backTo);
}

route: 'bank-account/new',
getRoute: ({policyID, bankAccountID, backTo}: {policyID: string | undefined; bankAccountID?: number; backTo?: string}) => {
let queryString = '';
if (bankAccountID) {
return getUrlWithBackToParam(`bank-account/${stepToOpen}?bankAccountID=${bankAccountID}`, backTo);
queryString = `?bankAccountID=${bankAccountID}`;
} else if (policyID) {
queryString = `?policyID=${policyID}`;
}
// TODO this backTo comes from drilling it through bank account form screens
// should be removed once https://github.com/Expensify/App/pull/72219 is resolved

return getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}${subStepToOpen ? `&subStep=${subStepToOpen}` : ''}`, backTo);
return getUrlWithBackToParam(`bank-account/new${queryString}`, backTo);
},
},
BANK_ACCOUNT_ENTER_SIGNER_INFO: {
Expand Down Expand Up @@ -921,6 +906,18 @@ const ROUTES = {
return getUrlWithBackToParam(`${base}${pagePart}${subPagePart}${actionPart}${queryString}`, backTo);
},
},
BANK_ACCOUNT_USD_SETUP: {
route: 'bank-account/new/us/:page?/:subPage?/:action?',
getRoute: ({policyID, page, subPage, action, backTo}: {policyID?: string; page?: string; subPage?: string; action?: 'edit'; backTo?: string}) => {
const base = 'bank-account/new/us';
const pagePart = page ? `/${page}` : '';
const subPagePart = subPage ? `/${subPage}` : '';
const actionPart = action ? `/${action}` : '';
const queryString = policyID ? `?policyID=${policyID}` : '';

return getUrlWithBackToParam(`${base}${pagePart}${subPagePart}${actionPart}${queryString}`, backTo);
},
},
SETTINGS: 'settings',
SETTINGS_PROFILE: {
route: 'settings/profile',
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,7 @@ const SCREENS = {
DYNAMIC_PRIVATE_NOTES_LIST: 'Dynamic_PrivateNotes_List',
DYNAMIC_PRIVATE_NOTES_EDIT: 'Dynamic_PrivateNotes_Edit',
REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount',
REIMBURSEMENT_ACCOUNT_USD: 'Reimbursement_Account_USD',
REIMBURSEMENT_ACCOUNT_NON_USD: 'Reimbursement_Account_Non_USD',
REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO: 'Reimbursement_Account_Signer_Info',
REFERRAL_DETAILS: 'Referral_Details',
Expand Down
1 change: 1 addition & 0 deletions src/components/DatePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ function DatePicker({
anchorPosition={popoverPosition}
shouldPositionFromTop={!isInverted}
forwardedFSClass={forwardedFSClass}
shouldCloseWhenBrowserNavigationChanged
/>
</>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useTheme from '@hooks/useTheme';
import StatusBar from '@libs/StatusBar';
import CONST from '@src/CONST';
import BaseModal from './BaseModal';
import {withInternalPopstate} from './internalPopstateGuard';
import type BaseModalProps from './types';
import type {WindowState} from './types';

Expand Down Expand Up @@ -55,7 +56,7 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = (
if (!(window.history.state as WindowState)?.shouldGoBack) {
return;
}
window.history.back();
withInternalPopstate(() => window.history.back());
}, 0);
} else {
onModalHide();
Expand Down
19 changes: 19 additions & 0 deletions src/components/Modal/internalPopstateGuard.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add docs to both of these functions that explain what we can use this for?

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
let isInternal = false;

/** Returns true when the next `popstate` was triggered by our own `history.back()`, not a real user back navigation. */
function isInternalPopstateInProgress(): boolean {
return isInternal;
}

/** Runs `action` (e.g. `history.back()`) while flagging the resulting `popstate` as internal, so listeners can ignore it. */
function withInternalPopstate(action: () => void) {
isInternal = true;
const clear = () => {
isInternal = false;
window.removeEventListener('popstate', clear);
};
window.addEventListener('popstate', clear);
action();
}

export {isInternalPopstateInProgress, withInternalPopstate};
6 changes: 1 addition & 5 deletions src/components/Navigation/DebugTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import getFocusedLeafScreenName from '@libs/Navigation/helpers/getFocusedLeafScreenName';
import isTabRouteAtRoot from '@libs/Navigation/helpers/isTabRouteAtRoot';
import Navigation from '@libs/Navigation/Navigation';
import {getRouteForCurrentStep as getReimbursementAccountRouteForCurrentStep} from '@libs/ReimbursementAccountUtils';
import {getChatTabBrickRoadReportID} from '@libs/WorkspacesSettingsUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -97,10 +96,7 @@ function getSettingsRoute(status: IndicatorStatus | undefined, reimbursementAcco
case CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS:
return ROUTES.WORKSPACE_INITIAL.getRoute(policyIDWithErrors);
case CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS:
return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({
policyID: reimbursementAccount?.achData?.policyID,
stepToOpen: getReimbursementAccountRouteForCurrentStep(reimbursementAccount?.achData?.currentStep ?? CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT),
});
return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID: reimbursementAccount?.achData?.policyID});
case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS:
return ROUTES.SETTINGS_SUBSCRIPTION.route;
case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO:
Expand Down
6 changes: 6 additions & 0 deletions src/components/Popover/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useRef} from 'react';
import {createPortal} from 'react-dom';
import Modal from '@components/Modal';
import {isInternalPopstateInProgress} from '@components/Modal/internalPopstateGuard';
import {usePopoverActions, usePopoverState} from '@components/PopoverProvider';
import PopoverWithoutOverlay from '@components/PopoverWithoutOverlay';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand Down Expand Up @@ -59,6 +60,11 @@ function Popover(props: PopoverProps) {
if (!isVisible) {
return;
}
// A nested Modal closing itself fires history.back() to drop its guard entry;
// that popstate is not a real user navigation, so skip closing.
if (isInternalPopstateInProgress()) {
return;
}
onClose?.();
};
window.addEventListener('popstate', listener);
Expand Down
28 changes: 28 additions & 0 deletions src/hooks/useReimbursementAccountSubmitCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {useCallback, useEffect, useRef} from 'react';
import ONYXKEYS from '@src/ONYXKEYS';
import useOnyx from './useOnyx';

/**
* Defers navigation (onSubmit) until the reimbursement account API call completes.
* Instead of navigating to the next step immediately after firing the API call,
* this hook waits for `isLoading` to go back to `false` and checks for errors.
*
* @param onSubmit - callback that navigates to the next step
* @returns markSubmitting - call this right after firing the API action
*/
export default function useReimbursementAccountSubmitCallback(onSubmit?: () => void) {
const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const isSubmittingRef = useRef(false);

useEffect(() => {
if (!isSubmittingRef.current || reimbursementAccount?.isLoading || reimbursementAccount?.errors) {
return;
}
isSubmittingRef.current = false;
onSubmit?.();
}, [reimbursementAccount?.isLoading, reimbursementAccount?.errors, onSubmit]);

return useCallback(() => {
isSubmittingRef.current = true;
}, []);
}
15 changes: 15 additions & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {useCallback} from 'react';
import {View} from 'react-native';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {isMobileChrome} from '@libs/Browser';
import withAgentAccessDenied from '@libs/Navigation/AppNavigator/withAgentAccessDenied';
import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
Expand Down Expand Up @@ -64,6 +65,16 @@ const loadAttachmentModalScreen = () => require<ReactComponentModule>('../../../

type Screens = Partial<Record<Screen, () => React.ComponentType>>;

const IS_MOBILE_CHROME = isMobileChrome();

const REIMBURSEMENT_ACCOUNT_FLOW_SCREENS: Screen[] = [
SCREENS.REIMBURSEMENT_ACCOUNT,
SCREENS.REIMBURSEMENT_ACCOUNT_USD,
SCREENS.REIMBURSEMENT_ACCOUNT_NON_USD,
SCREENS.REIMBURSEMENT_ACCOUNT_VERIFY_ACCOUNT,
SCREENS.REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO,
];

const OPTIONS_PER_SCREEN: Partial<Record<Screen, PlatformStackNavigationOptions>> = {
[SCREENS.SETTINGS.MERGE_ACCOUNTS.MERGE_RESULT]: {
animationTypeForReplace: 'push',
Expand Down Expand Up @@ -131,6 +142,9 @@ const OPTIONS_PER_SCREEN: Partial<Record<Screen, PlatformStackNavigationOptions>
[SCREENS.TWO_FACTOR_AUTH.SUCCESS]: {
animationTypeForReplace: 'push',
},
// Reimbursement Account flow animations glitch on low-end Android devices in Chrome mWeb so we intentionally disable them here.
// see https://github.com/Expensify/App/issues/87658
...(IS_MOBILE_CHROME ? Object.fromEntries(REIMBURSEMENT_ACCOUNT_FLOW_SCREENS.map((screen) => [screen, {animation: Animations.NONE}])) : {}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...(IS_MOBILE_CHROME ? Object.fromEntries(REIMBURSEMENT_ACCOUNT_FLOW_SCREENS.map((screen) => [screen, {animation: Animations.NONE}])) : {}),
// Reimbursement Account flow animations show glitches on low-end Android devices in Chrome mWeb so we intentionally disable them here.
// See https://github.com/Expensify/App/issues/89915
...(IS_MOBILE_CHROME ? Object.fromEntries(REIMBURSEMENT_ACCOUNT_FLOW_SCREENS.map((screen) => [screen, {animation: Animations.NONE}])) : {}),

};

/**
Expand Down Expand Up @@ -740,6 +754,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ITEMS]: () => require<ReactComponentModule>('../../../../pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage').default,
[SCREENS.CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT_ROOT]: () => require<ReactComponentModule>('@pages/workspace/ConnectExistingBusinessBankAccountPage').default,
[SCREENS.REIMBURSEMENT_ACCOUNT]: withAgentAccessDenied(() => require<ReactComponentModule>('../../../../pages/ReimbursementAccount/ReimbursementAccountPage').default),
[SCREENS.REIMBURSEMENT_ACCOUNT_USD]: () => require<ReactComponentModule>('../../../../pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlowPage').default,
[SCREENS.REIMBURSEMENT_ACCOUNT_NON_USD]: withAgentAccessDenied(
() => require<ReactComponentModule>('../../../../pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlowPage').default,
),
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,10 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
},
[SCREENS.REIMBURSEMENT_ACCOUNT_USD]: {
path: ROUTES.BANK_ACCOUNT_USD_SETUP.route,
exact: true,
},
[SCREENS.REIMBURSEMENT_ACCOUNT_NON_USD]: {
path: ROUTES.BANK_ACCOUNT_NON_USD_SETUP.route,
exact: true,
Expand Down
8 changes: 8 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2321,6 +2321,14 @@ type ReimbursementAccountNavigatorParamList = {
bankAccountID?: string;
subStep?: typeof CONST.BANK_ACCOUNT.STEP.COUNTRY;
};
[SCREENS.REIMBURSEMENT_ACCOUNT_USD]: {
page?: string;
subPage?: string;
action?: 'edit';
policyID?: string;
// eslint-disable-next-line no-restricted-syntax -- backTo is a temporary param will be removed after https://github.com/Expensify/App/issues/73825 is done
backTo?: Routes;
};
[SCREENS.REIMBURSEMENT_ACCOUNT_NON_USD]: {
page?: string;
subPage?: string;
Expand Down
25 changes: 2 additions & 23 deletions src/libs/ReimbursementAccountUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {ACHDataReimbursementAccount, ReimbursementAccountStep} from '@src/types/onyx/ReimbursementAccount';
import type {ACHDataReimbursementAccount} from '@src/types/onyx/ReimbursementAccount';

type ReimbursementAccountStepToOpen = ValueOf<typeof REIMBURSEMENT_ACCOUNT_ROUTE_NAMES> | '';

Expand All @@ -14,27 +14,6 @@ const REIMBURSEMENT_ACCOUNT_ROUTE_NAMES = {
NEW: 'new',
} as const;

function getRouteForCurrentStep(currentStep: ReimbursementAccountStep): ReimbursementAccountStepToOpen {
switch (currentStep) {
case CONST.BANK_ACCOUNT.STEP.COMPANY:
return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.COMPANY;
case CONST.BANK_ACCOUNT.STEP.REQUESTOR:
return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.PERSONAL_INFORMATION;
case CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS:
return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.BENEFICIAL_OWNERS;
case CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT:
return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.CONTRACT;
case CONST.BANK_ACCOUNT.STEP.VALIDATION:
return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.VALIDATE;
case CONST.BANK_ACCOUNT.STEP.ENABLE:
return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.ENABLE;
case CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT:
case CONST.BANK_ACCOUNT.STEP.COUNTRY:
default:
return REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW;
}
}

/**
* Returns true if a VBBA exists in any state other than OPEN or LOCKED
*/
Expand Down Expand Up @@ -67,5 +46,5 @@ const hasInProgressVBBA = (achData?: ACHDataReimbursementAccount, isNonUSDWorksp
return hasInProgressUSDVBBA(achData);
};

export {getBankAccountIDAsNumber, getRouteForCurrentStep, hasInProgressUSDVBBA, hasInProgressVBBA, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES};
export {getBankAccountIDAsNumber, hasInProgressUSDVBBA, hasInProgressVBBA, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES};
export type {ReimbursementAccountStepToOpen};
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@ function resetUSDBankAccount(
{
onyxMethod: Onyx.METHOD.SET,
key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
value: CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA,
value: {
...CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA,
achData: {
...CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA.achData,
policyID,
},
},
},
{
onyxMethod: Onyx.METHOD.SET,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ type ConnectedVerifiedBankAccountProps = {
onBackButtonPress: () => void;

/** Method to set the state of shouldShowConnectedVerifiedBankAccount */
setShouldShowConnectedVerifiedBankAccount: (shouldShowConnectedVerifiedBankAccount: boolean) => void;
setShouldShowConnectedVerifiedBankAccount?: (shouldShowConnectedVerifiedBankAccount: boolean) => void;

/** Method to set the state of USD bank account step */
setUSDBankAccountStep: (step: string | null) => void;
setUSDBankAccountStep?: (step: string | null) => void;

/** Whether the workspace currency is set to non USD currency */
isNonUSDWorkspace: boolean;
Expand Down
Loading
Loading