diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d458d94012e7..8c8b2e3fdbc1 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -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', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 92c5f63aa775..2caa6453d25f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -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'; @@ -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: { @@ -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', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3ec194ad5a61..8b16aaf10c0e 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -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', diff --git a/src/components/DatePicker/index.tsx b/src/components/DatePicker/index.tsx index ef1a17889f85..c3f813054e2a 100644 --- a/src/components/DatePicker/index.tsx +++ b/src/components/DatePicker/index.tsx @@ -175,6 +175,7 @@ function DatePicker({ anchorPosition={popoverPosition} shouldPositionFromTop={!isInverted} forwardedFSClass={forwardedFSClass} + shouldCloseWhenBrowserNavigationChanged /> ); diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 332c92cd71a3..f2d799a5ef54 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -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'; @@ -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(); diff --git a/src/components/Modal/internalPopstateGuard.ts b/src/components/Modal/internalPopstateGuard.ts new file mode 100644 index 000000000000..96cdd3d52533 --- /dev/null +++ b/src/components/Modal/internalPopstateGuard.ts @@ -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}; diff --git a/src/components/Navigation/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx index 454123719a64..837cecadcfe3 100644 --- a/src/components/Navigation/DebugTabView.tsx +++ b/src/components/Navigation/DebugTabView.tsx @@ -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'; @@ -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: diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index e5849d72d6a7..2434545ddb77 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -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'; @@ -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); diff --git a/src/hooks/useReimbursementAccountSubmitCallback.ts b/src/hooks/useReimbursementAccountSubmitCallback.ts new file mode 100644 index 000000000000..90fdc0026166 --- /dev/null +++ b/src/hooks/useReimbursementAccountSubmitCallback.ts @@ -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; + }, []); +} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c68128702d8c..11bdb5dc7fef 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -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'; @@ -64,6 +65,16 @@ const loadAttachmentModalScreen = () => require('../../../ type Screens = Partial 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> = { [SCREENS.SETTINGS.MERGE_ACCOUNTS.MERGE_RESULT]: { animationTypeForReplace: 'push', @@ -131,6 +142,9 @@ const OPTIONS_PER_SCREEN: Partial [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}])) : {}), }; /** @@ -740,6 +754,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage').default, [SCREENS.CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT_ROOT]: () => require('@pages/workspace/ConnectExistingBusinessBankAccountPage').default, [SCREENS.REIMBURSEMENT_ACCOUNT]: withAgentAccessDenied(() => require('../../../../pages/ReimbursementAccount/ReimbursementAccountPage').default), + [SCREENS.REIMBURSEMENT_ACCOUNT_USD]: () => require('../../../../pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlowPage').default, [SCREENS.REIMBURSEMENT_ACCOUNT_NON_USD]: withAgentAccessDenied( () => require('../../../../pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlowPage').default, ), diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 6d88c576a397..33e27496a2e2 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1183,6 +1183,10 @@ const config: LinkingOptions['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, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3a3d2525d0e6..8f855f69817c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -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; diff --git a/src/libs/ReimbursementAccountUtils.ts b/src/libs/ReimbursementAccountUtils.ts index e0f1dd03be60..4bce69e731c0 100644 --- a/src/libs/ReimbursementAccountUtils.ts +++ b/src/libs/ReimbursementAccountUtils.ts @@ -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 | ''; @@ -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 */ @@ -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}; diff --git a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts index 15d5e13f43f4..b1b3a6df7190 100644 --- a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts @@ -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, diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index a426ad5ffa4f..50f622603918 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -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; diff --git a/src/pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlowPage.tsx b/src/pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlowPage.tsx index 8d675eac39e9..f8b065860289 100644 --- a/src/pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlowPage.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlowPage.tsx @@ -121,7 +121,7 @@ function NonUSDVerifiedBankAccountFlowPage({route}: NonUSDVerifiedBankAccountFlo }, [backTo, currentPageIndex, pages, policyID]); return ( - + { - setShouldShowContinueSetupButton(false); - setUSDBankAccountStep(currentStep); + const stepToPageName: Record = { + [CONST.BANK_ACCOUNT.STEP.COUNTRY]: CONST.BANK_ACCOUNT.PAGE_NAMES.COUNTRY, + [CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT]: CONST.BANK_ACCOUNT.PAGE_NAMES.BANK_ACCOUNT, + [CONST.BANK_ACCOUNT.STEP.REQUESTOR]: CONST.BANK_ACCOUNT.PAGE_NAMES.REQUESTOR, + [CONST.BANK_ACCOUNT.STEP.COMPANY]: CONST.BANK_ACCOUNT.PAGE_NAMES.COMPANY, + [CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS]: CONST.BANK_ACCOUNT.PAGE_NAMES.BENEFICIAL_OWNERS, + [CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT]: CONST.BANK_ACCOUNT.PAGE_NAMES.ACH_CONTRACT, + [CONST.BANK_ACCOUNT.STEP.VALIDATION]: CONST.BANK_ACCOUNT.PAGE_NAMES.VALIDATION, + }; + const page = stepToPageName[currentStep] ?? CONST.BANK_ACCOUNT.PAGE_NAMES.COUNTRY; + Navigation.navigate(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID: policyIDParam, page, backTo})); }); - }, [currentStep]); + }, [currentStep, policyIDParam, backTo]); const continueNonUSDVBBASetup = () => { const {page: startPage, subPage: startSubPage} = getStartPageForContinueSetup(achData, nonUSDCountryDraftValue, policyCurrency, reimbursementAccountDraft); @@ -529,20 +531,6 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: ); } - if (!isNonUSDSetup && USDBankAccountStep !== null) { - return ( - - ); - } - return ( { - Navigation.goBack(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo, subStepToOpen: CONST.BANK_ACCOUNT.STEP.COUNTRY}), {compareParams: false}); + Navigation.goBack(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo}), {compareParams: false}); }} /> ); diff --git a/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx b/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx index 8ff529ef084d..a5b3dc71988f 100644 --- a/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx +++ b/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx @@ -2,8 +2,7 @@ import React, {useEffect, useRef} from 'react'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import useReimbursementAccountSubmitCallback from '@hooks/useReimbursementAccountSubmitCallback'; import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; @@ -20,27 +19,22 @@ type BankInfoProps = { /** Goes to the previous step */ onBackButtonPress: () => void; + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; + /** Current Policy ID */ policyID: string; - - /** Set the step of the USD verified bank account flow */ - setUSDBankAccountStep: (step: string | null) => void; -}; - -type BankInfoSubStepProps = SubStepProps & { - setUSDBankAccountStep: (step: string | null) => void; }; const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; -const manualSubSteps: Array> = [Manual]; -const plaidSubSteps: Array> = [Plaid]; const receivedRedirectURI = getPlaidOAuthReceivedRedirectURI(); -function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfoProps) { +function BankInfo({onBackButtonPress, onSubmit, policyID}: BankInfoProps) { const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const [plaidLinkToken] = useOnyx(ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN); const {translate} = useLocalize(); + const markSubmitting = useReimbursementAccountSubmitCallback(onSubmit); const redirectedFromPlaidToManualRef = useRef(false); const values = getSubStepValues(BANK_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount ?? {}); @@ -90,10 +84,10 @@ function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfo policyID, ); } + markSubmitting(); }; - const bodyContent = setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID ? plaidSubSteps : manualSubSteps; - const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo} = useSubStep({bodyContent, startFrom: 0, onFinished: submit}); + const BankInfoPage = setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID ? Plaid : Manual; // Some services user connects to via Plaid return dummy account numbers and routing numbers e.g. Chase // In this case we need to redirect user to manual flow to enter real account number and routing number @@ -108,12 +102,8 @@ function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfo }, [setupType, values.bankName]); const handleBackButtonPress = () => { - if (screenIndex === 0) { - onBackButtonPress(); - hideBankAccountErrors(); - } else { - prevScreen(); - } + onBackButtonPress(); + hideBankAccountErrors(); }; return ( @@ -125,12 +115,7 @@ function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfo startStepIndex={1} stepNames={CONST.BANK_ACCOUNT.STEP_NAMES} > - + ); } diff --git a/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Manual.tsx b/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Manual.tsx index 6e89550cb6d8..53a090418c49 100644 --- a/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Manual.tsx +++ b/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Manual.tsx @@ -8,21 +8,19 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getFieldRequiredErrors, isValidRoutingNumber} from '@libs/ValidationUtils'; import ExampleCheckImage from '@pages/ReimbursementAccount/USD/BankInfo/ExampleCheck'; +import type BankInfoSubStepProps from '@pages/ReimbursementAccount/USD/BankInfo/types'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -type ManualProps = SubStepProps; - const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; const STEP_FIELDS = [BANK_INFO_STEP_KEYS.ROUTING_NUMBER, BANK_INFO_STEP_KEYS.ACCOUNT_NUMBER]; -function Manual({onNext}: ManualProps) { +function Manual({onNext}: BankInfoSubStepProps) { const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); diff --git a/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Plaid.tsx b/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Plaid.tsx index d1719b8047fa..b64b07e70a45 100644 --- a/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Plaid.tsx +++ b/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Plaid.tsx @@ -6,21 +6,17 @@ import InputWrapper from '@components/Form/InputWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; -import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; +import type BankInfoSubStepProps from '@pages/ReimbursementAccount/USD/BankInfo/types'; import {setBankAccountSubStep, validatePlaidSelection} from '@userActions/BankAccounts'; import {updateReimbursementAccountDraft} from '@userActions/ReimbursementAccount'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -type PlaidProps = SubStepProps & { - setUSDBankAccountStep: (step: string | null) => void; -}; - const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; -function Plaid({onNext, setUSDBankAccountStep}: PlaidProps) { +function Plaid({onNext}: BankInfoSubStepProps) { const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA); @@ -61,12 +57,10 @@ function Plaid({onNext, setUSDBankAccountStep}: PlaidProps) { return; } setBankAccountSubStep(null); - setUSDBankAccountStep(null); - }, [isFocused, prevIsFocused, plaidData?.bankAccounts, setUSDBankAccountStep]); + }, [isFocused, prevIsFocused, plaidData?.bankAccounts]); const handlePlaidExit = () => { setBankAccountSubStep(null); - setUSDBankAccountStep(null); }; return ( diff --git a/src/pages/ReimbursementAccount/USD/BankInfo/types.ts b/src/pages/ReimbursementAccount/USD/BankInfo/types.ts new file mode 100644 index 000000000000..e79203da7611 --- /dev/null +++ b/src/pages/ReimbursementAccount/USD/BankInfo/types.ts @@ -0,0 +1,6 @@ +type BankInfoSubStepProps = { + /** Continues to the next step */ + onNext: (data?: unknown) => void; +}; + +export default BankInfoSubStepProps; diff --git a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormPages.tsx b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormPages.tsx new file mode 100644 index 000000000000..5ea2a967a3ae --- /dev/null +++ b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormPages.tsx @@ -0,0 +1,123 @@ +import React, {useCallback} from 'react'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useSubPage from '@hooks/useSubPage'; +import type {SubPageProps} from '@hooks/useSubPage/types'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import AddressUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/AddressUBO'; +import ConfirmationUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/ConfirmationUBO'; +import DateOfBirthUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/DateOfBirthUBO'; +import LegalNameUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/LegalNameUBO'; +import SocialSecurityNumberUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/SocialSecurityNumberUBO'; + +const PAGE_NAMES = CONST.BANK_ACCOUNT.PAGE_NAMES; +const SUB_PAGE_NAMES = CONST.BANK_ACCOUNT.BENEFICIAL_OWNERS_STEP.SUB_PAGE_NAMES; + +type BeneficialOwnerSubPageProps = SubPageProps & { + beneficialOwnerBeingModifiedID: string; + setBeneficialOwnerBeingModifiedID?: (id: string) => void; +}; + +const pages = [ + {pageName: SUB_PAGE_NAMES.LEGAL_NAME, component: LegalNameUBO}, + {pageName: SUB_PAGE_NAMES.DATE_OF_BIRTH, component: DateOfBirthUBO}, + {pageName: SUB_PAGE_NAMES.SSN, component: SocialSecurityNumberUBO}, + {pageName: SUB_PAGE_NAMES.ADDRESS, component: AddressUBO}, + {pageName: SUB_PAGE_NAMES.CONFIRMATION, component: ConfirmationUBO}, +]; + +type BeneficialOwnerDetailsFormPagesProps = { + /** ID of current policy */ + policyID?: string; + + /** ID of the beneficial owner being modified */ + beneficialOwnerBeingModifiedID: string; + + /** Setter for the beneficial owner being modified */ + setBeneficialOwnerBeingModifiedID: (id: string) => void; + + /** Whether user is editing an already-created beneficial owner */ + isEditingCreatedBeneficialOwner: boolean; + + /** Callback triggered after the last form page is completed */ + onFinished: () => void; + + /** Back to URL for preserving navigation context */ + backTo?: string; +}; + +function BeneficialOwnerDetailsFormPages({ + policyID, + beneficialOwnerBeingModifiedID, + setBeneficialOwnerBeingModifiedID, + isEditingCreatedBeneficialOwner, + onFinished, + backTo, +}: BeneficialOwnerDetailsFormPagesProps) { + const {translate} = useLocalize(); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + const hasExistingBeneficialOwners = (reimbursementAccountDraft?.beneficialOwnerKeys ?? []).length > 0; + + const buildRoute = useCallback( + (pageName: string, action?: 'edit') => ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: PAGE_NAMES.BENEFICIAL_OWNERS, subPage: pageName, action, backTo}), + [policyID, backTo], + ); + + const {CurrentPage, isEditing, currentPageName, pageIndex, prevPage, nextPage, moveTo, isRedirecting} = useSubPage({ + pages, + startFrom: 0, + onFinished, + buildRoute, + }); + + const handleBackButtonPress = useCallback(() => { + if (isEditing) { + Navigation.goBack(buildRoute(SUB_PAGE_NAMES.CONFIRMATION)); + return; + } + + if (pageIndex === 0) { + if (isEditingCreatedBeneficialOwner) { + Navigation.goBack(buildRoute(SUB_PAGE_NAMES.UBOS_LIST)); + } else if (hasExistingBeneficialOwners) { + Navigation.goBack(buildRoute(SUB_PAGE_NAMES.ARE_THERE_MORE_UBOS)); + } else { + Navigation.goBack(buildRoute(SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO)); + } + } else { + prevPage(); + } + }, [buildRoute, isEditing, isEditingCreatedBeneficialOwner, pageIndex, prevPage, hasExistingBeneficialOwners]); + + if (isRedirecting) { + return ; + } + + return ( + + + + ); +} + +export default BeneficialOwnerDetailsFormPages; diff --git a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx index 83e6e31c1616..ee0f5404c74b 100644 --- a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx +++ b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx @@ -1,37 +1,46 @@ import {Str} from 'expensify-common'; -import React, {useState} from 'react'; +import React, {useCallback, useEffect} from 'react'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import YesNoStep from '@components/SubStepForms/YesNoStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import useReimbursementAccountSubmitCallback from '@hooks/useReimbursementAccountSubmitCallback'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; import {updateBeneficialOwnersForBankAccount} from '@userActions/BankAccounts'; import {setDraftValues} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import SafeString from '@src/utils/SafeString'; -import AddressUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/AddressUBO'; -import ConfirmationUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/ConfirmationUBO'; -import DateOfBirthUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/DateOfBirthUBO'; -import LegalNameUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/LegalNameUBO'; -import SocialSecurityNumberUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/SocialSecurityNumberUBO'; +import BeneficialOwnerDetailsFormPages from './BeneficialOwnerDetailsFormPages'; import CompanyOwnersListUBO from './subSteps/CompanyOwnersListUBO'; type BeneficialOwnersStepProps = { /** Goes to the previous step */ onBackButtonPress: () => void; -}; -type BeneficialOwnerSubStepProps = SubStepProps & {beneficialOwnerBeingModifiedID: string; setBeneficialOwnerBeingModifiedID?: (id: string) => void}; + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; + + /** Name of the current sub page */ + currentSubPage?: string; + + /** ID of current policy */ + policyID?: string; + + /** Back to URL for preserving navigation context */ + backTo?: string; +}; -const SUBSTEP = CONST.BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.SUBSTEP; +const PAGE_NAMES = CONST.BANK_ACCOUNT.PAGE_NAMES; +const SUB_PAGE_NAMES = CONST.BANK_ACCOUNT.BENEFICIAL_OWNERS_STEP.SUB_PAGE_NAMES; const MAX_NUMBER_OF_UBOS = 4; -const bodyContent: Array> = [LegalNameUBO, DateOfBirthUBO, SocialSecurityNumberUBO, AddressUBO, ConfirmationUBO]; -function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { +const OUTER_SUB_PAGES = new Set([SUB_PAGE_NAMES.IS_USER_UBO, SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO, SUB_PAGE_NAMES.ARE_THERE_MORE_UBOS, SUB_PAGE_NAMES.UBOS_LIST]); + +function BeneficialOwnersStep({onBackButtonPress, onSubmit, currentSubPage, policyID, backTo}: BeneficialOwnersStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -39,27 +48,53 @@ function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const companyName = reimbursementAccount?.achData?.companyName ?? ''; - const policyID = reimbursementAccount?.achData?.policyID; - const defaultValues = { - ownsMoreThan25Percent: reimbursementAccount?.achData?.ownsMoreThan25Percent ?? reimbursementAccountDraft?.ownsMoreThan25Percent ?? false, - hasOtherBeneficialOwners: reimbursementAccount?.achData?.hasOtherBeneficialOwners ?? reimbursementAccountDraft?.hasOtherBeneficialOwners ?? false, - beneficialOwnerKeys: reimbursementAccount?.achData?.beneficialOwnerKeys ?? reimbursementAccountDraft?.beneficialOwnerKeys ?? [], - }; - - // We're only reading beneficialOwnerKeys from draft values because there is not option to remove UBO - // if we were to set them based on values saved in BE then there would be no option to enter different UBOs - // user would always see the same UBOs that was saved in BE when returning to this step and trying to change something - const [beneficialOwnerKeys, setBeneficialOwnerKeys] = useState(defaultValues.beneficialOwnerKeys); - const [beneficialOwnerBeingModifiedID, setBeneficialOwnerBeingModifiedID] = useState(''); - const [isEditingCreatedBeneficialOwner, setIsEditingCreatedBeneficialOwner] = useState(false); - const [isUserUBO, setIsUserUBO] = useState(defaultValues.ownsMoreThan25Percent); - const [isAnyoneElseUBO, setIsAnyoneElseUBO] = useState(defaultValues.hasOtherBeneficialOwners); - const [currentUBOSubStep, setCurrentUBOSubStep] = useState(1); + const markSubmitting = useReimbursementAccountSubmitCallback(onSubmit); + + // Read state from Onyx draft so it survives URL-based navigation (component remounts) + const isUserUBO = reimbursementAccountDraft?.ownsMoreThan25Percent ?? reimbursementAccount?.achData?.ownsMoreThan25Percent ?? false; + const beneficialOwners = reimbursementAccount?.achData?.beneficialOwners; + const isAnyoneElseUBO = beneficialOwners?.length ? true : (reimbursementAccountDraft?.hasOtherBeneficialOwners ?? false); + const beneficialOwnerKeys: string[] = reimbursementAccountDraft?.beneficialOwnerKeys ?? reimbursementAccount?.achData?.beneficialOwnerKeys ?? []; + // eslint-disable-next-line rulesdir/no-default-id-values + const beneficialOwnerBeingModifiedID = reimbursementAccountDraft?.ownerBeingModifiedID ?? ''; + const isEditingCreatedBeneficialOwner = reimbursementAccountDraft?.isEditingCreatedOwner ?? false; const canAddMoreUBOS = beneficialOwnerKeys.length < (isUserUBO ? MAX_NUMBER_OF_UBOS - 1 : MAX_NUMBER_OF_UBOS); + const hasCompletedBeneficialOwnersStep = reimbursementAccount?.achData?.ownsMoreThan25Percent !== undefined; + + // Redirect to the correct sub-page if no subPage is in the URL + useEffect(() => { + if (currentSubPage) { + return; + } + + let subPage: string = SUB_PAGE_NAMES.IS_USER_UBO; + if (isUserUBO || (isAnyoneElseUBO && beneficialOwnerKeys.length > 0)) { + subPage = SUB_PAGE_NAMES.UBOS_LIST; + } else if (hasCompletedBeneficialOwnersStep) { + subPage = SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO; + } + + Navigation.setParams({subPage} as Record); + }, [currentSubPage, policyID, backTo, isAnyoneElseUBO, beneficialOwnerKeys.length, isUserUBO, hasCompletedBeneficialOwnersStep]); + + const navigateToSubPage = useCallback( + (subPage: string) => { + Navigation.navigate(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: PAGE_NAMES.BENEFICIAL_OWNERS, subPage, backTo})); + }, + [policyID, backTo], + ); + + const navigateBackToSubPage = useCallback( + (subPage: string) => { + Navigation.goBack(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: PAGE_NAMES.BENEFICIAL_OWNERS, subPage, backTo})); + }, + [policyID, backTo], + ); + const submit = () => { const beneficialOwnerFields = ['firstName', 'lastName', 'dob', 'ssnLast4', 'street', 'city', 'state', 'zipCode']; - const beneficialOwners = beneficialOwnerKeys.map((ownerKey) => + const beneficialOwnersData = beneficialOwnerKeys.map((ownerKey) => beneficialOwnerFields.reduce( (acc, fieldName) => { acc[fieldName] = reimbursementAccountDraft ? SafeString(reimbursementAccountDraft[`beneficialOwner_${ownerKey}_${fieldName}`]) : undefined; @@ -73,75 +108,56 @@ function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { getBankAccountIDAsNumber(reimbursementAccount?.achData), { ownsMoreThan25Percent: isUserUBO, - beneficialOwners: JSON.stringify(beneficialOwners), + beneficialOwners: JSON.stringify(beneficialOwnersData), beneficialOwnerKeys, }, policyID, ); + markSubmitting(); }; const addBeneficialOwner = (beneficialOwnerID: string) => { - // Each beneficial owner is assigned a unique key that will connect it to values in saved ONYX. - // That way we can dynamically render each Identity Form based on which keys are present in the beneficial owners array. const newBeneficialOwners = [...beneficialOwnerKeys, beneficialOwnerID]; - - setBeneficialOwnerKeys(newBeneficialOwners); - setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {beneficialOwners: JSON.stringify(newBeneficialOwners)}); + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {beneficialOwnerKeys: newBeneficialOwners, beneficialOwners: JSON.stringify(newBeneficialOwners)}); }; + const handleBeneficialOwnerDetailsFormSubmit = () => { const shouldAddBeneficialOwner = !beneficialOwnerKeys.find((beneficialOwnerID) => beneficialOwnerID === beneficialOwnerBeingModifiedID) && canAddMoreUBOS; - if (shouldAddBeneficialOwner) { + if (shouldAddBeneficialOwner && beneficialOwnerBeingModifiedID) { addBeneficialOwner(beneficialOwnerBeingModifiedID); } - // Because beneficialOwnerKeys array is not yet updated at this point we need to check against lower MAX_NUMBER_OF_UBOS (account for the one that is being added) const isLastUBOThatCanBeAdded = beneficialOwnerKeys.length === (isUserUBO ? MAX_NUMBER_OF_UBOS - 2 : MAX_NUMBER_OF_UBOS - 1); - setCurrentUBOSubStep(isEditingCreatedBeneficialOwner || isLastUBOThatCanBeAdded ? SUBSTEP.UBOS_LIST : SUBSTEP.ARE_THERE_MORE_UBOS); - setIsEditingCreatedBeneficialOwner(false); + const nextSubPage = isEditingCreatedBeneficialOwner || isLastUBOThatCanBeAdded ? SUB_PAGE_NAMES.UBOS_LIST : SUB_PAGE_NAMES.ARE_THERE_MORE_UBOS; + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {isEditingCreatedOwner: false}); + navigateToSubPage(nextSubPage); }; - const { - componentToRender: BeneficialOwnerDetailsForm, - isEditing, - screenIndex, - nextScreen, - prevScreen, - moveTo, - resetScreenIndex, - goToTheLastStep, - } = useSubStep({ - bodyContent, - startFrom: 0, - onFinished: handleBeneficialOwnerDetailsFormSubmit, - }); - const prepareBeneficialOwnerDetailsForm = () => { const beneficialOwnerID = Str.guid(); - setBeneficialOwnerBeingModifiedID(beneficialOwnerID); - // Reset Beneficial Owner Details Form to first subStep - resetScreenIndex(); - setCurrentUBOSubStep(SUBSTEP.UBO_DETAILS_FORM); + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {ownerBeingModifiedID: beneficialOwnerID}); + navigateToSubPage(SUB_PAGE_NAMES.LEGAL_NAME); }; const handleNextUBOSubstep = (value: boolean) => { - if (currentUBOSubStep === SUBSTEP.IS_USER_UBO) { - setIsUserUBO(value); + if (currentSubPage === SUB_PAGE_NAMES.IS_USER_UBO) { + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {ownsMoreThan25Percent: value}); // User is an owner but there are 4 other owners already added, so we remove last one if (value && beneficialOwnerKeys.length === 4) { - setBeneficialOwnerKeys((previousBeneficialOwners) => previousBeneficialOwners.slice(0, 3)); + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {beneficialOwnerKeys: beneficialOwnerKeys.slice(0, 3)}); } - setCurrentUBOSubStep(SUBSTEP.IS_ANYONE_ELSE_UBO); + navigateToSubPage(SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO); return; } - if (currentUBOSubStep === SUBSTEP.IS_ANYONE_ELSE_UBO) { - setIsAnyoneElseUBO(value); + if (currentSubPage === SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO) { + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {hasOtherBeneficialOwners: value}); if (!canAddMoreUBOS && value) { - setCurrentUBOSubStep(SUBSTEP.UBOS_LIST); + navigateToSubPage(SUB_PAGE_NAMES.UBOS_LIST); return; } @@ -150,66 +166,62 @@ function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { return; } - // User is not an owner and no one else is an owner if (!isUserUBO && !value) { submit(); return; } - // User is an owner and no one else is an owner if (isUserUBO && !value) { - setCurrentUBOSubStep(SUBSTEP.UBOS_LIST); + navigateToSubPage(SUB_PAGE_NAMES.UBOS_LIST); return; } } - // Are there more UBOs - if (currentUBOSubStep === SUBSTEP.ARE_THERE_MORE_UBOS) { + if (currentSubPage === SUB_PAGE_NAMES.ARE_THERE_MORE_UBOS) { if (value) { prepareBeneficialOwnerDetailsForm(); return; } - setCurrentUBOSubStep(SUBSTEP.UBOS_LIST); - return; - } - - // User reached the limit of UBOs - if (currentUBOSubStep === SUBSTEP.UBO_DETAILS_FORM && !canAddMoreUBOS) { - setCurrentUBOSubStep(SUBSTEP.UBOS_LIST); + navigateToSubPage(SUB_PAGE_NAMES.UBOS_LIST); } }; const handleBackButtonPress = () => { - if (isEditing) { - goToTheLastStep(); - return; - } - - // User goes back to previous step - if (currentUBOSubStep === SUBSTEP.IS_USER_UBO) { + if (currentSubPage === SUB_PAGE_NAMES.IS_USER_UBO) { onBackButtonPress(); - // User reached limit of UBOs and goes back to initial question about additional UBOs - } else if (currentUBOSubStep === SUBSTEP.UBOS_LIST && !canAddMoreUBOS) { - setCurrentUBOSubStep(SUBSTEP.IS_ANYONE_ELSE_UBO); - // User goes back to last radio button - } else if (currentUBOSubStep === SUBSTEP.UBOS_LIST && isAnyoneElseUBO) { - setCurrentUBOSubStep(SUBSTEP.ARE_THERE_MORE_UBOS); - } else if (currentUBOSubStep === SUBSTEP.UBOS_LIST && isUserUBO && !isAnyoneElseUBO) { - setCurrentUBOSubStep(SUBSTEP.IS_ANYONE_ELSE_UBO); - // User moves between subSteps of beneficial owner details form - } else if (currentUBOSubStep === SUBSTEP.UBO_DETAILS_FORM && screenIndex > 0) { - prevScreen(); + } else if (currentSubPage === SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO) { + navigateBackToSubPage(SUB_PAGE_NAMES.IS_USER_UBO); + } else if (currentSubPage === SUB_PAGE_NAMES.UBOS_LIST && !canAddMoreUBOS) { + navigateBackToSubPage(SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO); + } else if (currentSubPage === SUB_PAGE_NAMES.UBOS_LIST && isAnyoneElseUBO) { + navigateBackToSubPage(SUB_PAGE_NAMES.ARE_THERE_MORE_UBOS); + } else if (currentSubPage === SUB_PAGE_NAMES.UBOS_LIST && isUserUBO && !isAnyoneElseUBO) { + navigateBackToSubPage(SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO); } else { - setCurrentUBOSubStep((currentSubstep) => currentSubstep - 1); + Navigation.goBack(); } }; const handleUBOEdit = (beneficialOwnerID: string) => { - setBeneficialOwnerBeingModifiedID(beneficialOwnerID); - setIsEditingCreatedBeneficialOwner(true); - setCurrentUBOSubStep(SUBSTEP.UBO_DETAILS_FORM); + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {ownerBeingModifiedID: beneficialOwnerID, isEditingCreatedOwner: true}); + navigateToSubPage(SUB_PAGE_NAMES.LEGAL_NAME); }; + // If the current sub page is not an outer page, render the details form + if (currentSubPage && !OUTER_SUB_PAGES.has(currentSubPage)) { + return ( + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {ownerBeingModifiedID: id})} + isEditingCreatedBeneficialOwner={isEditingCreatedBeneficialOwner} + onFinished={handleBeneficialOwnerDetailsFormSubmit} + backTo={backTo} + /> + ); + } + return ( - {currentUBOSubStep === SUBSTEP.IS_USER_UBO && ( + {currentSubPage === SUB_PAGE_NAMES.IS_USER_UBO && ( )} - {currentUBOSubStep === SUBSTEP.IS_ANYONE_ELSE_UBO && ( + {currentSubPage === SUB_PAGE_NAMES.IS_ANYONE_ELSE_UBO && ( )} - {currentUBOSubStep === SUBSTEP.UBO_DETAILS_FORM && ( - - )} - - {currentUBOSubStep === SUBSTEP.ARE_THERE_MORE_UBOS && ( + {currentSubPage === SUB_PAGE_NAMES.ARE_THERE_MORE_UBOS && ( )} - {currentUBOSubStep === SUBSTEP.UBOS_LIST && ( + {currentSubPage === SUB_PAGE_NAMES.UBOS_LIST && ( ); } diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/BusinessInfo.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/BusinessInfo.tsx index be7ff00b0080..60e87e064fcf 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/BusinessInfo.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/BusinessInfo.tsx @@ -1,11 +1,14 @@ import {Str} from 'expensify-common'; import lodashPick from 'lodash/pick'; import React, {useCallback, useMemo} from 'react'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import useReimbursementAccountSubmitCallback from '@hooks/useReimbursementAccountSubmitCallback'; +import useSubPage from '@hooks/useSubPage'; +import type {SubPageProps} from '@hooks/useSubPage/types'; +import Navigation from '@libs/Navigation/Navigation'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; import {isValidWebsite} from '@libs/ValidationUtils'; @@ -14,6 +17,7 @@ import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues import {updateCompanyInformationForBankAccount} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import AddressBusiness from './subSteps/AddressBusiness'; import ConfirmationBusiness from './subSteps/ConfirmationBusiness'; @@ -29,24 +33,32 @@ import WebsiteBusiness from './subSteps/WebsiteBusiness'; type BusinessInfoProps = { /** Goes to the previous step */ onBackButtonPress: () => void; + + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; + + /** Back to URL for preserving navigation context */ + backTo?: string; }; const BUSINESS_INFO_STEP_KEYS = INPUT_IDS.BUSINESS_INFO_STEP; +const PAGE_NAMES = CONST.BANK_ACCOUNT.PAGE_NAMES; +const SUB_PAGE_NAMES = CONST.BANK_ACCOUNT.BUSINESS_INFO_STEP.SUB_PAGE_NAMES; -const bodyContent: Array> = [ - NameBusiness, - TaxIdBusiness, - WebsiteBusiness, - PhoneNumberBusiness, - AddressBusiness, - TypeBusiness, - IncorporationDateBusiness, - IncorporationStateBusiness, - IncorporationCode, - ConfirmationBusiness, +const pages = [ + {pageName: SUB_PAGE_NAMES.NAME, component: NameBusiness}, + {pageName: SUB_PAGE_NAMES.TAX_ID, component: TaxIdBusiness}, + {pageName: SUB_PAGE_NAMES.WEBSITE, component: WebsiteBusiness}, + {pageName: SUB_PAGE_NAMES.PHONE, component: PhoneNumberBusiness}, + {pageName: SUB_PAGE_NAMES.ADDRESS, component: AddressBusiness}, + {pageName: SUB_PAGE_NAMES.TYPE, component: TypeBusiness}, + {pageName: SUB_PAGE_NAMES.INCORPORATION_DATE, component: IncorporationDateBusiness}, + {pageName: SUB_PAGE_NAMES.INCORPORATION_STATE, component: IncorporationStateBusiness}, + {pageName: SUB_PAGE_NAMES.INCORPORATION_CODE, component: IncorporationCode}, + {pageName: SUB_PAGE_NAMES.CONFIRMATION, component: ConfirmationBusiness}, ]; -function BusinessInfo({onBackButtonPress}: BusinessInfoProps) { +function BusinessInfo({onBackButtonPress, onSubmit, backTo}: BusinessInfoProps) { const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); @@ -61,6 +73,7 @@ function BusinessInfo({onBackButtonPress}: BusinessInfoProps) { const policyID = reimbursementAccount?.achData?.policyID; const bankAccountID = getBankAccountIDAsNumber(reimbursementAccount?.achData); + const markSubmitting = useReimbursementAccountSubmitCallback(onSubmit); const values = useMemo(() => getSubStepValues(BUSINESS_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); const submit = useCallback( @@ -85,29 +98,39 @@ function BusinessInfo({onBackButtonPress}: BusinessInfoProps) { const isBankAccountVerifying = reimbursementAccount?.achData?.state === CONST.BANK_ACCOUNT.STATE.VERIFYING; const startFrom = useMemo(() => (isBankAccountVerifying ? 0 : getInitialSubStepForBusinessInfo(values)), [values, isBankAccountVerifying]); - const { - componentToRender: SubStep, - isEditing, - screenIndex, - nextScreen, - prevScreen, - moveTo, - goToTheLastStep, - } = useSubStep({bodyContent, startFrom, onFinished: () => submit(true), onNextSubStep: () => submit(false)}); + const buildRoute = useCallback( + (pageName: string, action?: 'edit') => ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: PAGE_NAMES.COMPANY, subPage: pageName, action, backTo}), + [policyID, backTo], + ); + + const {CurrentPage, isEditing, currentPageName, pageIndex, nextPage, prevPage, moveTo, isRedirecting} = useSubPage({ + pages, + startFrom, + onFinished: () => { + submit(true); + markSubmitting(); + }, + onPageChange: () => submit(false), + buildRoute, + }); const handleBackButtonPress = () => { if (isEditing) { - goToTheLastStep(); + Navigation.goBack(buildRoute(SUB_PAGE_NAMES.CONFIRMATION)); return; } - if (screenIndex === 0) { + if (pageIndex === 0) { onBackButtonPress(); } else { - prevScreen(); + prevPage(); } }; + if (isRedirecting) { + return ; + } + return ( - ); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/AddressBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/AddressBusiness.tsx index 9d68d1da372f..69fc14e84caa 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/AddressBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/AddressBusiness.tsx @@ -4,7 +4,7 @@ import AddressStep from '@components/SubStepForms/AddressStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -21,7 +21,7 @@ const INPUT_KEYS = { const STEP_FIELDS = [COMPANY_BUSINESS_INFO_KEY.STREET, COMPANY_BUSINESS_INFO_KEY.CITY, COMPANY_BUSINESS_INFO_KEY.STATE, COMPANY_BUSINESS_INFO_KEY.ZIP_CODE]; -function AddressBusiness({onNext, onMove, isEditing}: SubStepProps) { +function AddressBusiness({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount, reimbursementAccountResult] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/ConfirmationBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/ConfirmationBusiness.tsx index 4bdeebd55e21..2ce528bc2118 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/ConfirmationBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/ConfirmationBusiness.tsx @@ -10,7 +10,7 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; @@ -35,7 +35,7 @@ function ConfirmCompanyLabel() { ); } -function ConfirmationBusiness({onNext, onMove}: SubStepProps) { +function ConfirmationBusiness({onNext, onMove}: SubPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -106,7 +106,11 @@ function ConfirmationBusiness({onNext, onMove}: SubStepProps) { /> { onMove(BUSINESS_INFO_STEP_INDEXES.COMPANY_TYPE); @@ -122,7 +126,7 @@ function ConfirmationBusiness({onNext, onMove}: SubStepProps) { /> { onMove(BUSINESS_INFO_STEP_INDEXES.INCORPORATION_STATE); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationCode.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationCode.tsx index 0fb2c08c063a..272bff25298d 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationCode.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationCode.tsx @@ -6,7 +6,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {isValidIndustryCode} from '@libs/ValidationUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -16,7 +16,7 @@ import IndustryCodeSelector from './IndustryCode/IndustryCodeSelector'; const COMPANY_INCORPORATION_CODE_KEY = INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_CODE; const STEP_FIELDS = [COMPANY_INCORPORATION_CODE_KEY]; -function IncorporationCode({onNext, isEditing}: SubStepProps) { +function IncorporationCode({onNext, isEditing}: SubPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationDateBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationDateBusiness.tsx index 2febf55e42b5..7674f9b770d6 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationDateBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationDateBusiness.tsx @@ -8,7 +8,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import useThemeStyles from '@hooks/useThemeStyles'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getFieldRequiredErrors, isValidDate, isValidPastDate} from '@libs/ValidationUtils'; @@ -19,7 +19,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; const COMPANY_INCORPORATION_DATE_KEY = INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_DATE; const STEP_FIELDS = [COMPANY_INCORPORATION_DATE_KEY]; -function IncorporationDateBusiness({onNext, isEditing}: SubStepProps) { +function IncorporationDateBusiness({onNext, isEditing}: SubPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationStateBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationStateBusiness.tsx index 5d10d77469b0..24d899552ea9 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationStateBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationStateBusiness.tsx @@ -9,7 +9,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import useThemeStyles from '@hooks/useThemeStyles'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; @@ -25,7 +25,7 @@ const validate = ( translate: LocalizedTranslate, ): FormInputErrors => getFieldRequiredErrors(values, STEP_FIELDS, translate); -function IncorporationStateBusiness({onNext, isEditing}: SubStepProps) { +function IncorporationStateBusiness({onNext, isEditing}: SubPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IndustryCode/IndustryCodeSelector.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IndustryCode/IndustryCodeSelector.tsx index 1584b9e5b93f..b5ced220b5bd 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IndustryCode/IndustryCodeSelector.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IndustryCode/IndustryCodeSelector.tsx @@ -1,9 +1,12 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import {useFocusEffect} from '@react-navigation/native'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; +import type {ListItem, SelectionListHandle} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; import {ALL_NAICS, NAICS, NAICS_MAPPING_WITH_ID} from '@src/NAICS'; type IndustryCodeSelectorProps = { @@ -14,11 +17,25 @@ type IndustryCodeSelectorProps = { function IndustryCodeSelector({onInputChange, value, errorText}: IndustryCodeSelectorProps) { const styles = useThemeStyles(); + const selectionListRef = useRef>(null); const [searchValue, setSearchValue] = useState(value); + const [isReady, setIsReady] = useState(false); const [shouldDisplayChildItems, setShouldDisplayChildItems] = useState(false); const {translate} = useLocalize(); + // Delay rendering the list and focusing the input until the screen transition animation completes. + useFocusEffect( + useCallback(() => { + const timeout = setTimeout(() => { + setIsReady(true); + selectionListRef.current?.focusTextInput(); + }, CONST.ANIMATED_TRANSITION); + + return () => clearTimeout(timeout); + }, []), + ); + const codeOptions = useMemo(() => { if (!searchValue) { return NAICS.map((item) => { @@ -65,6 +82,7 @@ function IndustryCodeSelector({onInputChange, value, errorText}: IndustryCodeSel onInputChange?.(val); }, value: searchValue, + disableAutoFocus: true, errorText, }), [errorText, onInputChange, searchValue, translate], @@ -73,7 +91,8 @@ function IndustryCodeSelector({onInputChange, value, errorText}: IndustryCodeSel return ( { setSearchValue(item.value); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/NameBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/NameBusiness.tsx index 6e18b09b2923..fd819f39875d 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/NameBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/NameBusiness.tsx @@ -4,7 +4,7 @@ import SingleFieldStep from '@components/SubStepForms/SingleFieldStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import {getFieldRequiredErrors, isValidCompanyName} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -13,7 +13,7 @@ import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; const COMPANY_NAME_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_NAME; const STEP_FIELDS = [COMPANY_NAME_KEY]; -function NameBusiness({onNext, onMove, isEditing}: SubStepProps) { +function NameBusiness({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -63,6 +63,7 @@ function NameBusiness({onNext, onMove, isEditing}: SubStepProps) { shouldUseDefaultValue={shouldDisableCompanyName} disabled={shouldDisableCompanyName} shouldShowHelpLinks={false} + shouldDelayAutoFocus /> ); } diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/PhoneNumberBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/PhoneNumberBusiness.tsx index 4437c83e6228..85dec7f7c1f0 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/PhoneNumberBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/PhoneNumberBusiness.tsx @@ -5,7 +5,7 @@ import SingleFieldStep from '@components/SubStepForms/SingleFieldStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getFieldRequiredErrors, isValidUSPhone} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; @@ -16,7 +16,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; const COMPANY_PHONE_NUMBER_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_PHONE; const STEP_FIELDS = [COMPANY_PHONE_NUMBER_KEY]; -function PhoneNumberBusiness({onNext, onMove, isEditing}: SubStepProps) { +function PhoneNumberBusiness({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount, reimbursementAccountResult] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -67,6 +67,7 @@ function PhoneNumberBusiness({onNext, onMove, isEditing}: SubStepProps) { defaultValue={defaultCompanyPhoneNumber} shouldShowHelpLinks={false} placeholder={translate('common.phoneNumberPlaceholder')} + shouldDelayAutoFocus /> ); } diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TaxIdBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TaxIdBusiness.tsx index 94a85398e8fb..5e3e9b13f73e 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TaxIdBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TaxIdBusiness.tsx @@ -5,7 +5,7 @@ import SingleFieldStep from '@components/SubStepForms/SingleFieldStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getFieldRequiredErrors, isValidTaxID} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; @@ -15,7 +15,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; const COMPANY_TAX_ID_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_TAX_ID; const STEP_FIELDS = [COMPANY_TAX_ID_KEY]; -function TaxIdBusiness({onNext, onMove, isEditing}: SubStepProps) { +function TaxIdBusiness({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount, reimbursementAccountResult] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -77,6 +77,7 @@ function TaxIdBusiness({onNext, onMove, isEditing}: SubStepProps) { shouldShowHelpLinks={false} placeholder={translate('businessInfoStep.taxIDNumberPlaceholder')} inputMode={CONST.INPUT_MODE.NUMERIC} + shouldDelayAutoFocus /> ); } diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TypeBusiness/TypeBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TypeBusiness/TypeBusiness.tsx index f8a296965496..0154ec35c188 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TypeBusiness/TypeBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TypeBusiness/TypeBusiness.tsx @@ -7,7 +7,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import useThemeStyles from '@hooks/useThemeStyles'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; @@ -19,7 +19,7 @@ import BusinessTypePicker from './BusinessTypePicker'; const COMPANY_INCORPORATION_TYPE_KEY = INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_TYPE; const STEP_FIELDS = [COMPANY_INCORPORATION_TYPE_KEY]; -function TypeBusiness({onNext, isEditing}: SubStepProps) { +function TypeBusiness({onNext, isEditing}: SubPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/WebsiteBusiness.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/WebsiteBusiness.tsx index 8a15cbcf8908..7214ee4aa687 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/WebsiteBusiness.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/WebsiteBusiness.tsx @@ -6,7 +6,7 @@ import SingleFieldStep from '@components/SubStepForms/SingleFieldStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import {getDefaultCompanyWebsite} from '@libs/BankAccountUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getFieldRequiredErrors, isValidWebsite} from '@libs/ValidationUtils'; @@ -19,7 +19,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; const COMPANY_WEBSITE_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_WEBSITE; const STEP_FIELDS = [COMPANY_WEBSITE_KEY]; -function WebsiteBusiness({onNext, onMove, isEditing}: SubStepProps) { +function WebsiteBusiness({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount, reimbursementAccountResult] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const isLoadingReimbursementAccount = isLoadingOnyxValue(reimbursementAccountResult); @@ -74,6 +74,7 @@ function WebsiteBusiness({onNext, onMove, isEditing}: SubStepProps) { defaultValue={defaultCompanyWebsite} inputMode={CONST.INPUT_MODE.URL} shouldShowHelpLinks={false} + shouldDelayAutoFocus /> ); } diff --git a/src/pages/ReimbursementAccount/USD/CompleteVerification/CompleteVerification.tsx b/src/pages/ReimbursementAccount/USD/CompleteVerification/CompleteVerification.tsx index 8b99de40b19c..a428397b24b1 100644 --- a/src/pages/ReimbursementAccount/USD/CompleteVerification/CompleteVerification.tsx +++ b/src/pages/ReimbursementAccount/USD/CompleteVerification/CompleteVerification.tsx @@ -1,10 +1,8 @@ import React, {useCallback, useMemo} from 'react'; -import type {ComponentType} from 'react'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import useReimbursementAccountSubmitCallback from '@hooks/useReimbursementAccountSubmitCallback'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; import {acceptACHContractForBankAccount} from '@userActions/BankAccounts'; @@ -16,12 +14,14 @@ import ConfirmAgreements from './subSteps/ConfirmAgreements'; type CompleteVerificationProps = { /** Handles back button press */ onBackButtonPress: () => void; + + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; }; const COMPLETE_VERIFICATION_KEYS = INPUT_IDS.COMPLETE_VERIFICATION; -const bodyContent: Array> = [ConfirmAgreements]; -function CompleteVerification({onBackButtonPress}: CompleteVerificationProps) { +function CompleteVerification({onBackButtonPress, onSubmit}: CompleteVerificationProps) { const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -31,6 +31,7 @@ function CompleteVerification({onBackButtonPress}: CompleteVerificationProps) { const values = useMemo(() => getSubStepValues(COMPLETE_VERIFICATION_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); const policyID = reimbursementAccount?.achData?.policyID; const bankAccountID = getBankAccountIDAsNumber(reimbursementAccount?.achData); + const markSubmitting = useReimbursementAccountSubmitCallback(onSubmit); const submit = useCallback(() => { acceptACHContractForBankAccount( @@ -38,27 +39,22 @@ function CompleteVerification({onBackButtonPress}: CompleteVerificationProps) { { isAuthorizedToUseBankAccount: values.isAuthorizedToUseBankAccount, certifyTrueInformation: values.certifyTrueInformation, - acceptTermsAndConditions: values.acceptTermsAndConditions, + acceptTermsAndConditions: (values.acceptTermsAndConditions || reimbursementAccount?.achData?.acceptTerms) ?? false, }, policyID, policyID ? lastPaymentMethod?.[policyID] : undefined, ); - }, [bankAccountID, values.isAuthorizedToUseBankAccount, values.certifyTrueInformation, values.acceptTermsAndConditions, policyID, lastPaymentMethod]); - - const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo, goToTheLastStep} = useSubStep({bodyContent, startFrom: 0, onFinished: submit}); - - const handleBackButtonPress = () => { - if (isEditing) { - goToTheLastStep(); - return; - } - - if (screenIndex === 0) { - onBackButtonPress(); - } else { - prevScreen(); - } - }; + markSubmitting(); + }, [ + bankAccountID, + values.isAuthorizedToUseBankAccount, + values.certifyTrueInformation, + values.acceptTermsAndConditions, + reimbursementAccount?.achData?.acceptTerms, + policyID, + lastPaymentMethod, + markSubmitting, + ]); return ( - + ); } diff --git a/src/pages/ReimbursementAccount/USD/CompleteVerification/subSteps/ConfirmAgreements.tsx b/src/pages/ReimbursementAccount/USD/CompleteVerification/subSteps/ConfirmAgreements.tsx index 0799cece1418..34b648cea0dc 100644 --- a/src/pages/ReimbursementAccount/USD/CompleteVerification/subSteps/ConfirmAgreements.tsx +++ b/src/pages/ReimbursementAccount/USD/CompleteVerification/subSteps/ConfirmAgreements.tsx @@ -7,15 +7,12 @@ import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getFieldRequiredErrors, isRequiredFulfilled} from '@libs/ValidationUtils'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -type ConfirmAgreementsProps = SubStepProps; - const COMPLETE_VERIFICATION_KEYS = INPUT_IDS.COMPLETE_VERIFICATION; const STEP_FIELDS = [ INPUT_IDS.COMPLETE_VERIFICATION.IS_AUTHORIZED_TO_USE_BANK_ACCOUNT, @@ -38,6 +35,11 @@ function TermsAndConditionsLabel() { return ; } +type ConfirmAgreementsProps = { + /** Continues to the next step */ + onNext: () => void; +}; + function ConfirmAgreements({onNext}: ConfirmAgreementsProps) { const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); @@ -50,7 +52,7 @@ function ConfirmAgreements({onNext}: ConfirmAgreementsProps) { const defaultValues = { isAuthorizedToUseBankAccount: confirmAgreementsValues.isAuthorizedToUseBankAccount ?? false, certifyTrueInformation: confirmAgreementsValues.certifyTrueInformation ?? false, - acceptTermsAndConditions: confirmAgreementsValues.acceptTermsAndConditions ?? false, + acceptTermsAndConditions: (confirmAgreementsValues.acceptTermsAndConditions || reimbursementAccount?.achData?.acceptTerms) ?? false, }; const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { diff --git a/src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx b/src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx index 5d44b1b79949..e64d20446ed0 100644 --- a/src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx +++ b/src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx @@ -1,18 +1,26 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; -import React from 'react'; +import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; +import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ReimbursementAccountLoadingIndicator from '@components/ReimbursementAccountLoadingIndicator'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useRootNavigationState from '@hooks/useRootNavigationState'; import useThemeStyles from '@hooks/useThemeStyles'; +import {isFullScreenName} from '@navigation/helpers/isNavigatorName'; +import Navigation from '@navigation/Navigation'; import ConnectedVerifiedBankAccount from '@pages/ReimbursementAccount/ConnectedVerifiedBankAccount'; import {navigateToConciergeChat} from '@userActions/Report'; import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import BankAccountValidationForm from './components/BankAccountValidationForm'; import FinishChatCard from './components/FinishChatCard'; @@ -21,15 +29,22 @@ type ConnectBankAccountProps = { onBackButtonPress: () => void; /** Method to set the state of shouldShowConnectedVerifiedBankAccount */ - setShouldShowConnectedVerifiedBankAccount: (shouldShowConnectedVerifiedBankAccount: boolean) => void; + setShouldShowConnectedVerifiedBankAccount?: (shouldShowConnectedVerifiedBankAccount: boolean) => void; /** Method to set the state of shouldShowConnectedVerifiedBankAccount */ - setUSDBankAccountStep: (step: string | null) => void; + setUSDBankAccountStep?: (step: string | null) => void; + + /** ID of current policy */ + policyID?: string; + + /** Route to return to when navigating back out of the flow */ + backTo?: Route; }; -function ConnectBankAccount({onBackButtonPress, setShouldShowConnectedVerifiedBankAccount, setUSDBankAccountStep}: ConnectBankAccountProps) { +function ConnectBankAccount({onBackButtonPress, setShouldShowConnectedVerifiedBankAccount, setUSDBankAccountStep, policyID, backTo}: ConnectBankAccountProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const topmostFullScreenRoute = useRootNavigationState((state) => state?.routes.findLast((lastRoute) => isFullScreenName(lastRoute.name))); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${reimbursementAccount?.achData?.policyID}`); @@ -42,13 +57,47 @@ function ConnectBankAccount({onBackButtonPress, setShouldShowConnectedVerifiedBa const handleNavigateToConciergeChat = () => navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas, true); const bankAccountState = reimbursementAccount?.achData?.state ?? ''; + const pendingAction = reimbursementAccount?.pendingAction; + + // After a disconnect, wait for the reset API to finish before navigating to the entry point. + const prevPendingActionRef = useRef(pendingAction); + useEffect(() => { + const prev = prevPendingActionRef.current; + prevPendingActionRef.current = pendingAction; + if (prev === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo})); + } + }, [pendingAction, policyID, backTo]); + + // While the disconnect is in flight, show the existing flow loader so the success screen doesn't re-render with cleared bank data + if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return ; + } // If a user tries to navigate directly to the validate page we'll show them the EnableStep if (bankAccountState === CONST.BANK_ACCOUNT.STATE.OPEN) { + if (topmostFullScreenRoute?.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR) { + return ( + + Navigation.dismissModal()} + /> + Navigation.dismissModal()} + /> + + ); + } return ( Navigation.dismissModal()} setShouldShowConnectedVerifiedBankAccount={setShouldShowConnectedVerifiedBankAccount} setUSDBankAccountStep={setUSDBankAccountStep} isNonUSDWorkspace={false} @@ -92,6 +141,8 @@ function ConnectBankAccount({onBackButtonPress, setShouldShowConnectedVerifiedBa requiresTwoFactorAuth={requiresTwoFactorAuth} reimbursementAccount={reimbursementAccount} setUSDBankAccountStep={setUSDBankAccountStep} + backTo={backTo} + policy={policy} /> )} diff --git a/src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/FinishChatCard.tsx b/src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/FinishChatCard.tsx index 62ae351d5384..840f07c0b9eb 100644 --- a/src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/FinishChatCard.tsx +++ b/src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/FinishChatCard.tsx @@ -1,5 +1,6 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import MenuItem from '@components/MenuItem'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; @@ -10,12 +11,15 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; import WorkspaceResetBankAccountModal from '@pages/workspace/WorkspaceResetBankAccountModal'; import {goToWithdrawalAccountSetupStep, requestResetBankAccount, setBankAccountSubStep} from '@userActions/BankAccounts'; import {navigateToConciergeChat} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReimbursementAccount} from '@src/types/onyx'; +import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; +import type {Policy, ReimbursementAccount} from '@src/types/onyx'; import Enable2FACard from './Enable2FACard'; type FinishChatCardProps = { @@ -25,11 +29,17 @@ type FinishChatCardProps = { /** Boolean required to display Enable2FACard component */ requiresTwoFactorAuth: boolean; + /** The policy which the user has access to and which the report is tied to */ + policy: OnyxEntry; + /** Method to set the state of USD bank account step */ - setUSDBankAccountStep: (step: string | null) => void; + setUSDBankAccountStep?: (step: string | null) => void; + + /** Route to return to when navigating back out of the flow */ + backTo?: Route; }; -function FinishChatCard({requiresTwoFactorAuth, reimbursementAccount, setUSDBankAccountStep}: FinishChatCardProps) { +function FinishChatCard({requiresTwoFactorAuth, reimbursementAccount, policy, setUSDBankAccountStep, backTo}: FinishChatCardProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -77,8 +87,15 @@ function FinishChatCard({requiresTwoFactorAuth, reimbursementAccount, setUSDBank title={translate('workspace.bankAccount.updateDetails')} onPress={() => { setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL).then(() => { - setUSDBankAccountStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); + Navigation.navigate( + ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({ + policyID: policy?.id, + page: CONST.BANK_ACCOUNT.PAGE_NAMES.REQUESTOR, + subPage: CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.SUB_PAGE_NAMES.FULL_NAME, + backTo, + }), + ); }); }} outerWrapperStyle={shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8} diff --git a/src/pages/ReimbursementAccount/USD/Country/index.tsx b/src/pages/ReimbursementAccount/USD/Country/index.tsx index b4a28c913745..ded09899c388 100644 --- a/src/pages/ReimbursementAccount/USD/Country/index.tsx +++ b/src/pages/ReimbursementAccount/USD/Country/index.tsx @@ -7,24 +7,20 @@ type CountryProps = { /** Handles back button press */ onBackButtonPress: () => void; + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; + /** Array of step names */ stepNames: readonly string[]; - /** Method to set the state of setUSDBankAccountStep */ - setUSDBankAccountStep?: (step: string | null) => void; - /** ID of current policy */ policyID: string | undefined; }; -function Country({onBackButtonPress, stepNames, setUSDBankAccountStep, policyID}: CountryProps) { +function Country({onBackButtonPress, onSubmit, stepNames, policyID}: CountryProps) { const submit = () => { - if (!setUSDBankAccountStep) { - return; - } - - setUSDBankAccountStep(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT); goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT); + onSubmit?.(); }; return ( diff --git a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/PersonalInfo.tsx b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/PersonalInfo.tsx index bd548f912ff0..a0c39597cf35 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/PersonalInfo.tsx @@ -1,17 +1,21 @@ import type {ForwardedRef} from 'react'; import React, {useCallback, useMemo} from 'react'; import type {View} from 'react-native'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import useReimbursementAccountSubmitCallback from '@hooks/useReimbursementAccountSubmitCallback'; +import useSubPage from '@hooks/useSubPage'; +import type {SubPageProps} from '@hooks/useSubPage/types'; +import Navigation from '@libs/Navigation/Navigation'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; import getInitialSubStepForPersonalInfo from '@pages/ReimbursementAccount/USD/utils/getInitialSubStepForPersonalInfo'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; import {updatePersonalInformationForBankAccount} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import Address from './subSteps/Address'; import Confirmation from './subSteps/Confirmation'; @@ -23,14 +27,29 @@ type PersonalInfoProps = { /** Goes to the previous step */ onBackButtonPress: () => void; + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; + /** Reference to the outer element */ ref?: ForwardedRef; + + /** Back to URL for preserving navigation context */ + backTo?: string; }; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; -const bodyContent: Array> = [FullName, DateOfBirth, SocialSecurityNumber, Address, Confirmation]; +const PAGE_NAMES = CONST.BANK_ACCOUNT.PAGE_NAMES; +const SUB_PAGE_NAMES = CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.SUB_PAGE_NAMES; + +const pages = [ + {pageName: SUB_PAGE_NAMES.FULL_NAME, component: FullName}, + {pageName: SUB_PAGE_NAMES.DATE_OF_BIRTH, component: DateOfBirth}, + {pageName: SUB_PAGE_NAMES.SSN, component: SocialSecurityNumber}, + {pageName: SUB_PAGE_NAMES.ADDRESS, component: Address}, + {pageName: SUB_PAGE_NAMES.CONFIRMATION, component: Confirmation}, +]; -function PersonalInfo({onBackButtonPress, ref}: PersonalInfoProps) { +function PersonalInfo({onBackButtonPress, onSubmit, ref, backTo}: PersonalInfoProps) { const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -39,6 +58,7 @@ function PersonalInfo({onBackButtonPress, ref}: PersonalInfoProps) { const policyID = reimbursementAccount?.achData?.policyID; const values = useMemo(() => getSubStepValues(PERSONAL_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); const bankAccountID = getBankAccountIDAsNumber(reimbursementAccount?.achData); + const markSubmitting = useReimbursementAccountSubmitCallback(onSubmit); const submit = useCallback( (isConfirmPage: boolean) => { updatePersonalInformationForBankAccount(bankAccountID, {...values}, policyID, isConfirmPage); @@ -48,26 +68,32 @@ function PersonalInfo({onBackButtonPress, ref}: PersonalInfoProps) { const isBankAccountVerifying = reimbursementAccount?.achData?.state === CONST.BANK_ACCOUNT.STATE.VERIFYING; const startFrom = useMemo(() => (isBankAccountVerifying ? 0 : getInitialSubStepForPersonalInfo(values)), [values, isBankAccountVerifying]); - const { - componentToRender: SubStep, - isEditing, - screenIndex, - nextScreen, - prevScreen, - moveTo, - goToTheLastStep, - } = useSubStep({bodyContent, startFrom, onFinished: () => submit(true), onNextSubStep: () => submit(false)}); + const buildRoute = useCallback( + (pageName: string, action?: 'edit') => ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: PAGE_NAMES.REQUESTOR, subPage: pageName, action, backTo}), + [policyID, backTo], + ); + + const {CurrentPage, isEditing, currentPageName, pageIndex, nextPage, prevPage, moveTo, isRedirecting} = useSubPage({ + pages, + startFrom, + onFinished: () => { + submit(true); + markSubmitting(); + }, + onPageChange: () => submit(false), + buildRoute, + }); const handleBackButtonPress = () => { if (isEditing) { - goToTheLastStep(); + Navigation.goBack(buildRoute(SUB_PAGE_NAMES.CONFIRMATION)); return; } - if (screenIndex === 0) { + if (pageIndex === 0) { onBackButtonPress(); } else { - prevScreen(); + prevPage(); } }; @@ -82,11 +108,16 @@ function PersonalInfo({onBackButtonPress, ref}: PersonalInfoProps) { startStepIndex={2} stepNames={CONST.BANK_ACCOUNT.STEP_NAMES} > - + {isRedirecting ? ( + + ) : ( + + )} ); } diff --git a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/Address.tsx b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/Address.tsx index cc63886621ce..39c912e0ce38 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/Address.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/Address.tsx @@ -4,7 +4,7 @@ import AddressStep from '@components/SubStepForms/AddressStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -22,7 +22,7 @@ const INPUT_KEYS = { const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.STREET, PERSONAL_INFO_STEP_KEY.CITY, PERSONAL_INFO_STEP_KEY.STATE, PERSONAL_INFO_STEP_KEY.ZIP_CODE]; -function Address({onNext, onMove, isEditing}: SubStepProps) { +function Address({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount, reimbursementAccountResult] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); diff --git a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/Confirmation.tsx b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/Confirmation.tsx index 17c29db3ce12..e9c1a8749b9c 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/Confirmation.tsx @@ -2,7 +2,7 @@ import React, {useMemo} from 'react'; import ConfirmationStep from '@components/SubStepForms/ConfirmationStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; import CONST from '@src/CONST'; @@ -12,7 +12,7 @@ import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; const PERSONAL_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT.SUBSTEP_INDEX.PERSONAL_INFO; -function Confirmation({onNext, onMove, isEditing}: SubStepProps) { +function Confirmation({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); diff --git a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/DateOfBirth.tsx b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/DateOfBirth.tsx index cde22e01ed97..54adf2a4be87 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/DateOfBirth.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/DateOfBirth.tsx @@ -4,7 +4,7 @@ import DateOfBirthStep from '@components/SubStepForms/DateOfBirthStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import useThemeStyles from '@hooks/useThemeStyles'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import HelpLinks from '@pages/ReimbursementAccount/USD/Requestor/PersonalInfo/HelpLinks'; @@ -16,7 +16,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; const PERSONAL_INFO_DOB_KEY = INPUT_IDS.PERSONAL_INFO_STEP.DOB; const STEP_FIELDS = [PERSONAL_INFO_DOB_KEY]; -function DateOfBirth({onNext, onMove, isEditing}: SubStepProps) { +function DateOfBirth({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/FullName.tsx b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/FullName.tsx index a1f841963792..6093b3c2a77a 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/FullName.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/FullName.tsx @@ -3,7 +3,7 @@ import FullNameStep from '@components/SubStepForms/FullNameStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -11,7 +11,7 @@ import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME]; -function FullName({onNext, onMove, isEditing}: SubStepProps) { +function FullName({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); diff --git a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/SocialSecurityNumber.tsx b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/SocialSecurityNumber.tsx index ff9d07476a6a..6e7384be05ab 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/SocialSecurityNumber.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/PersonalInfo/subSteps/SocialSecurityNumber.tsx @@ -5,7 +5,7 @@ import SingleFieldStep from '@components/SubStepForms/SingleFieldStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getFieldRequiredErrors, isValidSSNLastFour} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; @@ -16,7 +16,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; -function SocialSecurityNumber({onNext, onMove, isEditing}: SubStepProps) { +function SocialSecurityNumber({onNext, onMove, isEditing}: SubPageProps) { const {translate} = useLocalize(); const [reimbursementAccount, reimbursementAccountResult] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -68,6 +68,7 @@ function SocialSecurityNumber({onNext, onMove, isEditing}: SubStepProps) { maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.SSN} enabledWhenOffline forwardedFSClass={CONST.FULLSTORY.CLASS.MASK} + shouldDelayAutoFocus /> ); } diff --git a/src/pages/ReimbursementAccount/USD/Requestor/RequestorStep.tsx b/src/pages/ReimbursementAccount/USD/Requestor/RequestorStep.tsx index 4a9de8d65704..f02b5186081d 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/RequestorStep.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/RequestorStep.tsx @@ -2,28 +2,28 @@ import React from 'react'; import type {ForwardedRef} from 'react'; import type {View} from 'react-native'; import PersonalInfo from './PersonalInfo/PersonalInfo'; -import VerifyIdentity from './VerifyIdentity/VerifyIdentity'; type RequestorStepProps = { /** Goes to the previous step */ onBackButtonPress: () => void; - /** If we should show Onfido flow */ - shouldShowOnfido: boolean; + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; /** Reference to the outer element */ ref?: ForwardedRef; -}; -function RequestorStep({shouldShowOnfido, onBackButtonPress, ref}: RequestorStepProps) { - if (shouldShowOnfido) { - return ; - } + /** Back to URL for preserving navigation context */ + backTo?: string; +}; +function RequestorStep({onBackButtonPress, onSubmit, ref, backTo}: RequestorStepProps) { return ( ); } diff --git a/src/pages/ReimbursementAccount/USD/Requestor/VerifyIdentity/VerifyIdentity.tsx b/src/pages/ReimbursementAccount/USD/Requestor/VerifyIdentity/VerifyIdentity.tsx index 5947ef3f01f8..c739dd63a433 100644 --- a/src/pages/ReimbursementAccount/USD/Requestor/VerifyIdentity/VerifyIdentity.tsx +++ b/src/pages/ReimbursementAccount/USD/Requestor/VerifyIdentity/VerifyIdentity.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import Onfido from '@components/Onfido'; @@ -8,18 +8,21 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import Growl from '@libs/Growl'; -import {clearOnfidoToken, goToWithdrawalAccountSetupStep, updateReimbursementAccountDraft, verifyIdentityForBankAccount} from '@userActions/BankAccounts'; +import {clearOnfidoToken, updateReimbursementAccountDraft, verifyIdentityForBankAccount} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; type VerifyIdentityProps = { /** Goes to the previous step */ onBackButtonPress: () => void; + + /** Navigates to the next step */ + onSubmit?: () => void; }; const ONFIDO_ERROR_DISPLAY_DURATION = 10000; -function VerifyIdentity({onBackButtonPress}: VerifyIdentityProps) { +function VerifyIdentity({onBackButtonPress, onSubmit}: VerifyIdentityProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -31,25 +34,37 @@ function VerifyIdentity({onBackButtonPress}: VerifyIdentityProps) { const policyID = reimbursementAccount?.achData?.policyID; const bankAccountID = reimbursementAccount?.achData?.bankAccountID; + const isOnfidoAlreadyComplete = useRef(reimbursementAccount?.achData?.isOnfidoSetupComplete); + const onSubmitRef = useRef(onSubmit); + + // If Onfido is already complete (e.g. direct URL navigation), skip to next step + useEffect(() => { + if (!isOnfidoAlreadyComplete.current) { + return; + } + onSubmitRef.current?.(); + }, []); + const handleOnfidoSuccess = useCallback( (onfidoData: OnfidoData) => { verifyIdentityForBankAccount(Number(bankAccountID), {...onfidoData, applicantID: onfidoApplicantID}, policyID); updateReimbursementAccountDraft({isOnfidoSetupComplete: true}); + onSubmit?.(); }, - [bankAccountID, onfidoApplicantID, policyID], + [bankAccountID, onfidoApplicantID, policyID, onSubmit], ); const handleOnfidoError = () => { // In case of any unexpected error we log it to the server, show a growl, and return the user back to the requestor step so they can try again. Growl.error(translate('onfidoStep.genericError'), ONFIDO_ERROR_DISPLAY_DURATION); clearOnfidoToken(); - goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); + onBackButtonPress(); }; const handleOnfidoUserExit = (isUserInitiated?: boolean) => { if (isUserInitiated) { clearOnfidoToken(); - goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); + onBackButtonPress(); } else { setOnfidoKey(Math.floor(Math.random() * 1000000)); } diff --git a/src/pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlow.tsx b/src/pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlow.tsx deleted file mode 100644 index c13784998b2a..000000000000 --- a/src/pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlow.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import BankInfo from './BankInfo/BankInfo'; -import BeneficialOwnersStep from './BeneficialOwnerInfo/BeneficialOwnersStep'; -import BusinessInfo from './BusinessInfo/BusinessInfo'; -import CompleteVerification from './CompleteVerification/CompleteVerification'; -import ConnectBankAccount from './ConnectBankAccount/ConnectBankAccount'; -import Country from './Country'; -import RequestorStep from './Requestor/RequestorStep'; - -type USDVerifiedBankAccountFlowProps = { - USDBankAccountStep: string; - policyID: string | undefined; - onBackButtonPress: () => void; - requestorStepRef: React.RefObject; - onfidoToken: string; - setUSDBankAccountStep: (step: string | null) => void; - setShouldShowConnectedVerifiedBankAccount: (shouldShowConnectedVerifiedBankAccount: boolean) => void; -}; - -function USDVerifiedBankAccountFlow({ - USDBankAccountStep, - policyID = '', - onBackButtonPress, - requestorStepRef, - onfidoToken, - setUSDBankAccountStep, - setShouldShowConnectedVerifiedBankAccount, -}: USDVerifiedBankAccountFlowProps) { - const styles = useThemeStyles(); - const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); - - let CurrentStep: React.JSX.Element | null; - switch (USDBankAccountStep) { - case CONST.BANK_ACCOUNT.STEP.COUNTRY: - CurrentStep = ( - - ); - break; - case CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT: - CurrentStep = ( - - ); - break; - case CONST.BANK_ACCOUNT.STEP.REQUESTOR: - CurrentStep = ( - - ); - break; - case CONST.BANK_ACCOUNT.STEP.COMPANY: - CurrentStep = ; - break; - case CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS: - CurrentStep = ; - break; - case CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT: - CurrentStep = ; - break; - case CONST.BANK_ACCOUNT.STEP.VALIDATION: - CurrentStep = ( - - ); - break; - default: - CurrentStep = null; - break; - } - - if (CurrentStep) { - return {CurrentStep}; - } - - return null; -} - -export default USDVerifiedBankAccountFlow; diff --git a/src/pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlowPage.tsx b/src/pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlowPage.tsx new file mode 100644 index 000000000000..d721d8155e1c --- /dev/null +++ b/src/pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlowPage.tsx @@ -0,0 +1,135 @@ +import React, {useCallback, useMemo, useRef} from 'react'; +import {View} from 'react-native'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {ReimbursementAccountNavigatorParamList} from '@libs/Navigation/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import BankInfo from './BankInfo/BankInfo'; +import BeneficialOwnersStep from './BeneficialOwnerInfo/BeneficialOwnersStep'; +import BusinessInfo from './BusinessInfo/BusinessInfo'; +import CompleteVerification from './CompleteVerification/CompleteVerification'; +import ConnectBankAccount from './ConnectBankAccount/ConnectBankAccount'; +import Country from './Country'; +import RequestorStep from './Requestor/RequestorStep'; +import VerifyIdentity from './Requestor/VerifyIdentity/VerifyIdentity'; +import type USDPageProps from './types'; + +const PAGE_NAMES = CONST.BANK_ACCOUNT.PAGE_NAMES; +const BANK_INFO_SUB_PAGES = CONST.BANK_ACCOUNT.BANK_INFO_STEP.SUB_PAGE_NAMES; +const PERSONAL_INFO_SUB_PAGES = CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.SUB_PAGE_NAMES; +const BUSINESS_INFO_SUB_PAGES = CONST.BANK_ACCOUNT.BUSINESS_INFO_STEP.SUB_PAGE_NAMES; +const BENEFICIAL_OWNERS_SUB_PAGES = CONST.BANK_ACCOUNT.BENEFICIAL_OWNERS_STEP.SUB_PAGE_NAMES; +const COMPLETE_VERIFICATION_SUB_PAGES = CONST.BANK_ACCOUNT.COMPLETE_VERIFICATION_STEP.SUB_PAGE_NAMES; + +type PageEntry = { + pageName: string; + component: React.ComponentType; + firstSubPage?: string; + lastSubPage?: string; +}; + +const pages: PageEntry[] = [ + {pageName: PAGE_NAMES.COUNTRY, component: Country as React.ComponentType}, + {pageName: PAGE_NAMES.BANK_ACCOUNT, component: BankInfo as React.ComponentType, firstSubPage: BANK_INFO_SUB_PAGES.PLAID, lastSubPage: BANK_INFO_SUB_PAGES.PLAID}, + { + pageName: PAGE_NAMES.REQUESTOR, + component: RequestorStep as React.ComponentType, + firstSubPage: PERSONAL_INFO_SUB_PAGES.FULL_NAME, + lastSubPage: PERSONAL_INFO_SUB_PAGES.CONFIRMATION, + }, + {pageName: PAGE_NAMES.VERIFY_IDENTITY, component: VerifyIdentity as React.ComponentType}, + { + pageName: PAGE_NAMES.COMPANY, + component: BusinessInfo as React.ComponentType, + firstSubPage: BUSINESS_INFO_SUB_PAGES.NAME, + lastSubPage: BUSINESS_INFO_SUB_PAGES.CONFIRMATION, + }, + { + pageName: PAGE_NAMES.BENEFICIAL_OWNERS, + component: BeneficialOwnersStep as React.ComponentType, + firstSubPage: BENEFICIAL_OWNERS_SUB_PAGES.IS_USER_UBO, + lastSubPage: undefined, + }, + { + pageName: PAGE_NAMES.ACH_CONTRACT, + component: CompleteVerification as React.ComponentType, + firstSubPage: COMPLETE_VERIFICATION_SUB_PAGES.CONFIRM_AGREEMENTS, + lastSubPage: COMPLETE_VERIFICATION_SUB_PAGES.CONFIRM_AGREEMENTS, + }, + {pageName: PAGE_NAMES.VALIDATION, component: ConnectBankAccount as React.ComponentType}, +]; + +type USDVerifiedBankAccountFlowPageProps = PlatformStackScreenProps; + +function USDVerifiedBankAccountFlowPage({route}: USDVerifiedBankAccountFlowPageProps) { + const styles = useThemeStyles(); + const policyID = route.params?.policyID; + const currentPage = route.params?.page; + const currentSubPage = route.params?.subPage; + const backTo = route.params?.backTo; + + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + + const requestorStepRef = useRef(null); + const isOnfidoSetupComplete = reimbursementAccount?.achData?.isOnfidoSetupComplete; + + const currentPageIndex = useMemo(() => { + const index = pages.findIndex((p) => p.pageName === currentPage); + return index >= 0 ? index : 0; + }, [currentPage]); + + const currentEntry = pages.at(currentPageIndex); + const CurrentPage = currentEntry?.component ?? (Country as React.ComponentType); + const isRequestorStep = currentEntry?.pageName === PAGE_NAMES.REQUESTOR; + + const shouldSkipVerifyIdentity = useCallback((pageName?: string) => pageName === PAGE_NAMES.VERIFY_IDENTITY && isOnfidoSetupComplete, [isOnfidoSetupComplete]); + + const onSubmit = useCallback(() => { + let nextIndex = currentPageIndex + 1; + if (shouldSkipVerifyIdentity(pages.at(nextIndex)?.pageName)) { + nextIndex += 1; + } + if (nextIndex >= pages.length) { + Navigation.goBack(); + return; + } + const nextPage = pages.at(nextIndex); + Navigation.navigate(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: nextPage?.pageName, subPage: nextPage?.firstSubPage, backTo})); + }, [backTo, currentPageIndex, policyID, shouldSkipVerifyIdentity]); + + const onBackButtonPress = useCallback(() => { + let prevIndex = currentPageIndex - 1; + if (shouldSkipVerifyIdentity(pages.at(prevIndex)?.pageName)) { + prevIndex -= 1; + } + if (prevIndex < 0) { + Navigation.goBack(); + return; + } + const prevPage = pages.at(prevIndex); + Navigation.goBack(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: prevPage?.pageName, subPage: prevPage?.lastSubPage, backTo})); + }, [backTo, currentPageIndex, policyID, shouldSkipVerifyIdentity]); + + return ( + + + + ); +} + +USDVerifiedBankAccountFlowPage.displayName = 'USDVerifiedBankAccountFlowPage'; + +export default USDVerifiedBankAccountFlowPage; diff --git a/src/pages/ReimbursementAccount/USD/types.ts b/src/pages/ReimbursementAccount/USD/types.ts new file mode 100644 index 000000000000..c808a68bff8f --- /dev/null +++ b/src/pages/ReimbursementAccount/USD/types.ts @@ -0,0 +1,28 @@ +import type {ForwardedRef} from 'react'; +import type {View} from 'react-native'; +import type {Route} from '@src/ROUTES'; + +type USDPageProps = { + /** Handles submit button press */ + onSubmit: () => void; + + /** Handles back button press */ + onBackButtonPress: () => void; + + /** ID of current policy */ + policyID?: string; + + /** Name of the current sub page */ + currentSubPage?: string; + + /** Array of step names for the progress indicator */ + stepNames?: readonly string[]; + + /** Reference to the outer element (used by RequestorStep) */ + ref?: ForwardedRef; + + /** Back to URL for preserving navigation context */ + backTo?: Route; +}; + +export default USDPageProps; diff --git a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx index 4da763c2981f..8e6a747efcc5 100644 --- a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx +++ b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx @@ -123,10 +123,10 @@ function VerifiedBankAccountFlowEntryPoint({ const prepareNextStep = useCallback( (setupType: ValueOf) => { setBankAccountSubStep(setupType); - setUSDBankAccountStep(CONST.BANK_ACCOUNT.STEP.COUNTRY); goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.COUNTRY); + Navigation.navigate(ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: CONST.BANK_ACCOUNT.PAGE_NAMES.COUNTRY, backTo})); }, - [setUSDBankAccountStep], + [policyID, backTo], ); /** diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx index dc08a6534505..24dd5d84896f 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx @@ -19,7 +19,6 @@ import {getLastFourDigits} from '@libs/BankAccountUtils'; import {getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from '@libs/CardUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -67,7 +66,6 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA Navigation.navigate( ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({ policyID, - stepToOpen: REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, backTo: ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID), }), ); diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx index 2c694a0253c8..063813a70834 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx @@ -20,7 +20,7 @@ import {getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; -import {hasInProgressUSDVBBA, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils'; +import {hasInProgressUSDVBBA} from '@libs/ReimbursementAccountUtils'; import Navigation from '@navigation/Navigation'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -71,7 +71,6 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif Navigation.navigate( ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({ policyID: policy?.id, - stepToOpen: REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, backTo: ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policy?.id), }), ); diff --git a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx index f8ce87367218..e4a6bc1b354f 100644 --- a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx +++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx @@ -30,7 +30,7 @@ import {getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {areTravelPersonalDetailsMissing} from '@libs/PersonalDetailsUtils'; -import {hasInProgressUSDVBBA, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils'; +import {hasInProgressUSDVBBA} from '@libs/ReimbursementAccountUtils'; import { getIsTravelInvoicingEnabled, getTravelInvoicingCardSettingsKey, @@ -175,7 +175,6 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec Navigation.navigate( ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({ policyID, - stepToOpen: REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, backTo: ROUTES.WORKSPACE_TRAVEL.getRoute(policyID), }), ); diff --git a/src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx b/src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx index 1203dfce00b2..981ab69e16ff 100644 --- a/src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx +++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx @@ -15,7 +15,6 @@ import {configureTravelInvoicingForPolicy, setTravelInvoicingSettlementAccount} import {getLastFourDigits} from '@libs/BankAccountUtils'; import {getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils'; import {getIsTravelInvoicingEnabled, getTravelInvoicingCardSettingsKey} from '@libs/TravelInvoicingUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -97,7 +96,6 @@ function WorkspaceTravelInvoicingSettlementAccountPage({route}: WorkspaceTravelI Navigation.navigate( ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({ policyID, - stepToOpen: REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, backTo: ROUTES.WORKSPACE_TRAVEL_SETTINGS_ACCOUNT.getRoute(policyID), }), ); diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index ba47e03db79f..a107c8dee8ad 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -175,6 +175,9 @@ type ACHData = Partial { - describe('getRouteForCurrentStep', () => { - it("should return 'new' step if 'BankAccountStep' or '' is provided", () => { - expect(getRouteForCurrentStep(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT)).toEqual(REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW); - expect(getRouteForCurrentStep('')).toEqual(REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW); - }); - }); - describe('getBankAccountIDAsNumber', () => { it('should return DEFAULT_NUMBER_ID when achData is undefined', () => { // Given no ACH data diff --git a/tests/unit/internalPopstateGuardTest.ts b/tests/unit/internalPopstateGuardTest.ts new file mode 100644 index 000000000000..875088da3e1b --- /dev/null +++ b/tests/unit/internalPopstateGuardTest.ts @@ -0,0 +1,74 @@ +import {isInternalPopstateInProgress, withInternalPopstate} from '@components/Modal/internalPopstateGuard'; + +describe('internalPopstateGuard', () => { + afterEach(() => { + // Drain any lingering popstate listener so the module-scoped flag is back to false before the next case. + window.dispatchEvent(new PopStateEvent('popstate')); + }); + + it('reports false by default', () => { + expect(isInternalPopstateInProgress()).toBe(false); + }); + + it('reports true synchronously after withInternalPopstate is called, before any popstate fires', () => { + withInternalPopstate(() => {}); + expect(isInternalPopstateInProgress()).toBe(true); + }); + + it('runs the action synchronously', () => { + const action = jest.fn(); + withInternalPopstate(action); + expect(action).toHaveBeenCalledTimes(1); + }); + + it('clears the flag after the next popstate event fires', () => { + withInternalPopstate(() => {}); + expect(isInternalPopstateInProgress()).toBe(true); + + window.dispatchEvent(new PopStateEvent('popstate')); + + expect(isInternalPopstateInProgress()).toBe(false); + }); + + it('detaches its popstate listener after one event (subsequent popstates do not flip the flag)', () => { + withInternalPopstate(() => {}); + window.dispatchEvent(new PopStateEvent('popstate')); + expect(isInternalPopstateInProgress()).toBe(false); + + window.dispatchEvent(new PopStateEvent('popstate')); + expect(isInternalPopstateInProgress()).toBe(false); + }); + + it('a listener registered before withInternalPopstate sees the flag as true during the same popstate', () => { + let observedDuringEvent: boolean | undefined; + const popoverListener = () => { + observedDuringEvent = isInternalPopstateInProgress(); + }; + window.addEventListener('popstate', popoverListener); + + withInternalPopstate(() => {}); + window.dispatchEvent(new PopStateEvent('popstate')); + + expect(observedDuringEvent).toBe(true); + expect(isInternalPopstateInProgress()).toBe(false); + + window.removeEventListener('popstate', popoverListener); + }); + + it('a listener registered after withInternalPopstate sees the flag already cleared', () => { + withInternalPopstate(() => {}); + + let observedDuringEvent: boolean | undefined; + const lateListener = () => { + observedDuringEvent = isInternalPopstateInProgress(); + }; + window.addEventListener('popstate', lateListener); + + window.dispatchEvent(new PopStateEvent('popstate')); + + // Clear listener was registered before lateListener, so by the time lateListener runs the flag is already released. + expect(observedDuringEvent).toBe(false); + + window.removeEventListener('popstate', lateListener); + }); +});