diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 149e27f62215a..76588faa3eafa 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -594,7 +594,65 @@ const CONST = { VALIDATION: 'ValidationStep', ENABLE: 'EnableStep', }, + PAGE_NAMES: { + COUNTRY: 'country', + BANK_ACCOUNT: 'bank-account', + REQUESTOR: 'requestor', + VERIFY_IDENTITY: 'verify-identity', + COMPANY: 'company', + BENEFICIAL_OWNERS: 'beneficial-owners', + 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: 'incorporation-date', + INCORPORATION_STATE: 'incorporation-state', + INCORPORATION_CODE: 'incorporation-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 039fa85e1dd04..f1b6fa03b63f8 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -14,7 +14,6 @@ import type {ReplacementReason} from './libs/actions/Card'; import type {IOURequestType} from './libs/actions/IOU'; 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'; @@ -295,36 +294,24 @@ const ROUTES = { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation 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; - }) => { + route: 'bank-account/new', + getRoute: ({policyID, bankAccountID, backTo}: {policyID: string | undefined; bankAccountID?: number; backTo?: string}) => { if (!policyID && !bankAccountID) { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - return getUrlWithBackToParam(`bank-account/${stepToOpen}`, backTo); + return getUrlWithBackToParam(`bank-account/new`, backTo); } if (bankAccountID) { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - return getUrlWithBackToParam(`bank-account/${stepToOpen}?bankAccountID=${bankAccountID}`, backTo); + return getUrlWithBackToParam(`bank-account/new?bankAccountID=${bankAccountID}`, backTo); } // 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 // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - return getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}${subStepToOpen ? `&subStep=${subStepToOpen}` : ''}`, backTo); + return getUrlWithBackToParam(`bank-account/new?policyID=${policyID}`, backTo); }, }, BANK_ACCOUNT_ENTER_SIGNER_INFO: { @@ -356,6 +343,19 @@ const ROUTES = { return getUrlWithBackToParam(`${base}${pagePart}${subPagePart}${actionPart}${queryString}`, backTo); }, }, + BANK_ACCOUNT_USD_SETUP: { + route: 'bank-account/new/us/:page?/:subPage?/:action?', + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + 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}` : ''; + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + 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 f1c979cf1562a..dbc429e20d909 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -906,6 +906,7 @@ const SCREENS = { }, FLAG_COMMENT_ROOT: 'FlagComment_Root', 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/Navigation/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx index 95003505a6a82..8e25ec0753107 100644 --- a/src/components/Navigation/DebugTabView.tsx +++ b/src/components/Navigation/DebugTabView.tsx @@ -16,7 +16,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import {getRouteForCurrentStep as getReimbursementAccountRouteForCurrentStep} from '@libs/ReimbursementAccountUtils'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import {getChatTabBrickRoadReportID} from '@libs/WorkspacesSettingsUtils'; import CONST from '@src/CONST'; @@ -82,10 +81,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/hooks/useReimbursementAccountSubmit.ts b/src/hooks/useReimbursementAccountSubmit.ts new file mode 100644 index 0000000000000..6fff0ec34c2ed --- /dev/null +++ b/src/hooks/useReimbursementAccountSubmit.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 useReimbursementAccountSubmit(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 c201e975ed1ac..8f0161653684f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -629,6 +629,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]: () => require('../../../../pages/ReimbursementAccount/ReimbursementAccountPage').default, + [SCREENS.REIMBURSEMENT_ACCOUNT_USD]: () => require('../../../../pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlowPage').default, [SCREENS.REIMBURSEMENT_ACCOUNT_NON_USD]: () => require('../../../../pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlowPage').default, [SCREENS.REIMBURSEMENT_ACCOUNT_VERIFY_ACCOUNT]: () => require('../../../../pages/ReimbursementAccount/ReimbursementAccountVerifyAccountPage').default, [SCREENS.REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO]: () => require('../../../../pages/ReimbursementAccount/EnterSignerInfo').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 219a83362e524..6de186b451204 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1168,6 +1168,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 3df6e35764ec4..3663a0f5cc43c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2262,6 +2262,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 0610e2d0dd10e..912ac6f24fcde 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, hasInProgressNonUSDVBBA, hasInProgressVBBA, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES}; +export {getBankAccountIDAsNumber, hasInProgressUSDVBBA, hasInProgressNonUSDVBBA, hasInProgressVBBA, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES}; export type {ReimbursementAccountStepToOpen}; diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index fdc8a0b8cec58..a800819754a31 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/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 760e7ae0a2156..942275f42a8c3 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -28,7 +28,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReimbursementAccountNavigatorParamList} from '@libs/Navigation/types'; import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; -import {getRouteForCurrentStep, hasInProgressUSDVBBA, hasInProgressVBBA} from '@libs/ReimbursementAccountUtils'; +import {hasInProgressUSDVBBA, hasInProgressVBBA} from '@libs/ReimbursementAccountUtils'; import shouldReopenOnfido from '@libs/shouldReopenOnfido'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {isFullScreenName} from '@navigation/helpers/isNavigatorName'; @@ -60,7 +60,6 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import ConnectedVerifiedBankAccount from './ConnectedVerifiedBankAccount'; import getStartPageForContinueSetup from './NonUSD/utils/getStartPageForContinueSetup'; -import USDVerifiedBankAccountFlow from './USD/USDVerifiedBankAccountFlow'; import getFieldsForStep from './USD/utils/getFieldsForStep'; import getStepToOpenFromRouteParams from './USD/utils/getStepToOpenFromRouteParams'; import VerifiedBankAccountFlowEntryPoint from './VerifiedBankAccountFlowEntryPoint'; @@ -77,7 +76,7 @@ const OFFLINE_ACCESSIBLE_STEPS = [ CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT, ] as const; -function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: ReimbursementAccountPageProps) { +function ReimbursementAccountPage({route, policy, isLoadingPolicy}: ReimbursementAccountPageProps) { const {environmentURL} = useEnvironment(); const session = useSession(); const [reimbursementAccount, reimbursementAccountMetadata] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -333,10 +332,6 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: // so we don't clear it. We only want to clear the errors if we are moving between steps. hideBankAccountErrors(); } - - // Use the current page navigation object to set the param to the correct route in the stack - const stepToOpen = getRouteForCurrentStep(currentStep); - navigation.setParams({stepToOpen}); }, // eslint-disable-next-line react-hooks/exhaustive-deps [isOffline, reimbursementAccount?.draftStep, reimbursementAccount?.pendingAction, reimbursementAccount?.isLoading, hasACHDataBeenLoaded, shouldShowContinueSetupButton, currentStep], @@ -347,9 +342,19 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: // so we're always showing manual setup with locked numbers he can not change setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL).then(() => { 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); @@ -519,20 +524,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 582a0c11b0641..b7a2afa21cfa5 100644 --- a/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx +++ b/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx @@ -1,9 +1,10 @@ -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, 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 useReimbursementAccountSubmit from '@hooks/useReimbursementAccountSubmit'; +import useSubPage from '@hooks/useSubPage'; +import type {SubPageProps} from '@hooks/useSubPage/types'; import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; @@ -11,6 +12,7 @@ import {connectBankAccountManually, connectBankAccountWithPlaid} from '@userActi import {hideBankAccountErrors} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type {ReimbursementAccountForm} from '@src/types/form'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import Manual from './subSteps/Manual'; @@ -20,27 +22,30 @@ 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; + /** Back to URL for preserving navigation context */ + backTo?: string; }; const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; -const manualSubSteps: Array> = [Manual]; -const plaidSubSteps: Array> = [Plaid]; +const PAGE_NAMES = CONST.BANK_ACCOUNT.PAGE_NAMES; +const SUB_PAGE_NAMES = CONST.BANK_ACCOUNT.BANK_INFO_STEP.SUB_PAGE_NAMES; const receivedRedirectURI = getPlaidOAuthReceivedRedirectURI(); -function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfoProps) { +const manualPages = [{pageName: SUB_PAGE_NAMES.MANUAL, component: Manual}]; +const plaidPages = [{pageName: SUB_PAGE_NAMES.PLAID, component: Plaid}]; + +function BankInfo({onBackButtonPress, onSubmit, policyID, backTo}: BankInfoProps) { const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const [plaidLinkToken] = useOnyx(ONYXKEYS.PLAID_LINK_TOKEN); const {translate} = useLocalize(); + const markSubmitting = useReimbursementAccountSubmit(onSubmit); const redirectedFromPlaidToManualRef = useRef(false); const values = getSubStepValues(BANK_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount ?? {}); @@ -84,11 +89,17 @@ function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfo policyID, ); } + markSubmitting(); }; - const bodyContent = setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID ? plaidSubSteps : manualSubSteps; - // eslint-disable-next-line @typescript-eslint/no-deprecated - const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo} = useSubStep({bodyContent, startFrom: 0, onFinished: submit}); + const pages = setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID ? plaidPages : manualPages; + + const buildRoute = useCallback( + (pageName: string, action?: 'edit') => ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: PAGE_NAMES.BANK_ACCOUNT, subPage: pageName, action, backTo}), + [policyID, backTo], + ); + + const {CurrentPage, isEditing, pageIndex, nextPage, prevPage, moveTo} = useSubPage({pages, startFrom: 0, onFinished: submit, buildRoute}); // 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 @@ -103,11 +114,11 @@ function BankInfo({onBackButtonPress, policyID, setUSDBankAccountStep}: BankInfo }, [setupType, values.bankName]); const handleBackButtonPress = () => { - if (screenIndex === 0) { + if (pageIndex === 0) { onBackButtonPress(); hideBankAccountErrors(); } else { - prevScreen(); + prevPage(); } }; @@ -120,11 +131,10 @@ 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 6e89550cb6d83..41048d40dab23 100644 --- a/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Manual.tsx +++ b/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Manual.tsx @@ -8,7 +8,7 @@ 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 type {SubPageProps} from '@hooks/useSubPage/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getFieldRequiredErrors, isValidRoutingNumber} from '@libs/ValidationUtils'; import ExampleCheckImage from '@pages/ReimbursementAccount/USD/BankInfo/ExampleCheck'; @@ -17,12 +17,10 @@ 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}: SubPageProps) { 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 0259a336648f1..711a73fa42014 100644 --- a/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Plaid.tsx +++ b/src/pages/ReimbursementAccount/USD/BankInfo/subSteps/Plaid.tsx @@ -6,7 +6,7 @@ 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 type {SubPageProps} from '@hooks/useSubPage/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; import {setBankAccountSubStep, validatePlaidSelection} from '@userActions/BankAccounts'; @@ -14,13 +14,9 @@ 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}: SubPageProps) { 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/BeneficialOwnerInfo/BeneficialOwnerDetailsFormPages.tsx b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormPages.tsx new file mode 100644 index 0000000000000..5be519b3f65b4 --- /dev/null +++ b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormPages.tsx @@ -0,0 +1,117 @@ +import React, {useCallback} from 'react'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useSubPage from '@hooks/useSubPage'; +import type {SubPageProps} from '@hooks/useSubPage/types'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +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 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 { + Navigation.goBack(buildRoute(SUB_PAGE_NAMES.IS_USER_UBO)); + } + } else { + prevPage(); + } + }, [buildRoute, isEditing, isEditingCreatedBeneficialOwner, pageIndex, prevPage]); + + 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 48e31dd63b53c..a984c54e54b69 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 useReimbursementAccountSubmit from '@hooks/useReimbursementAccountSubmit'; 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; -const SUBSTEP = CONST.BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.SUBSTEP; + /** ID of current policy */ + policyID?: string; + + /** Back to URL for preserving navigation context */ + backTo?: string; +}; + +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,44 @@ 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 = useReimbursementAccountSubmit(onSubmit); + + // Read state from Onyx draft so it survives URL-based navigation (component remounts) + const isUserUBO = reimbursementAccount?.achData?.ownsMoreThan25Percent ?? reimbursementAccountDraft?.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); + // Redirect to the correct sub-page if no subPage is in the URL + useEffect(() => { + if (currentSubPage) { + return; + } + const subPage = isUserUBO || (isAnyoneElseUBO && beneficialOwnerKeys.length > 0) ? SUB_PAGE_NAMES.UBOS_LIST : SUB_PAGE_NAMES.IS_USER_UBO; + Navigation.setParams({subPage} as Record); + }, [currentSubPage, policyID, backTo, isAnyoneElseUBO, beneficialOwnerKeys.length, isUserUBO]); + + 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,76 +99,55 @@ 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, - // eslint-disable-next-line @typescript-eslint/no-deprecated - } = 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; } @@ -151,66 +156,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 0eb937a1050c4..78026ce7d38c5 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 useReimbursementAccountSubmit from '@hooks/useReimbursementAccountSubmit'; +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 = useReimbursementAccountSubmit(onSubmit); const values = useMemo(() => getSubStepValues(BUSINESS_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); const submit = useCallback( @@ -85,30 +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, - // eslint-disable-next-line @typescript-eslint/no-deprecated - } = 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 9d68d1da372f3..69fc14e84caac 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 4bdeebd55e214..1c6b94b2337af 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(); diff --git a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationCode.tsx b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationCode.tsx index 579ac48913fe8..8cca170661eb7 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 2febf55e42b5c..7674f9b770d66 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 5d10d77469b0f..24d899552ea9f 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 113bd42714d6b..9876116569111 100644 --- a/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IndustryCode/IndustryCodeSelector.tsx +++ b/src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IndustryCode/IndustryCodeSelector.tsx @@ -1,9 +1,11 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/ListItem/RadioListItem'; +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 +16,22 @@ 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(); + useEffect(() => { + const timeout = setTimeout(() => { + setIsReady(true); + selectionListRef.current?.focusTextInput(); + }, CONST.ANIMATED_TRANSITION); + + return () => clearTimeout(timeout); + }, []); + const codeOptions = useMemo(() => { if (!searchValue) { return NAICS.map((item) => { @@ -62,6 +75,7 @@ function IndustryCodeSelector({onInputChange, value, errorText}: IndustryCodeSel onInputChange?.(val); }, value: searchValue, + disableAutoFocus: true, errorText, }), [errorText, onInputChange, searchValue, translate], @@ -70,7 +84,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 6e18b09b29235..fd819f39875da 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 4437c83e62284..85dec7f7c1f04 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 94a85398e8fba..5e3e9b13f73e6 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 f8a296965496f..0154ec35c1889 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 8a15cbcf8908e..7214ee4aa6879 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 5d544521ba211..821d17eeca623 100644 --- a/src/pages/ReimbursementAccount/USD/CompleteVerification/CompleteVerification.tsx +++ b/src/pages/ReimbursementAccount/USD/CompleteVerification/CompleteVerification.tsx @@ -1,27 +1,38 @@ 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 useReimbursementAccountSubmit from '@hooks/useReimbursementAccountSubmit'; +import useSubPage from '@hooks/useSubPage'; +import type {SubPageProps} from '@hooks/useSubPage/types'; +import Navigation from '@libs/Navigation/Navigation'; import {getBankAccountIDAsNumber} from '@libs/ReimbursementAccountUtils'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; import {acceptACHContractForBankAccount} 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 ConfirmAgreements from './subSteps/ConfirmAgreements'; type CompleteVerificationProps = { /** Handles back button press */ onBackButtonPress: () => void; + + /** Handles submit button press (URL-based navigation) */ + onSubmit?: () => void; + + /** Back to URL for preserving navigation context */ + backTo?: string; }; const COMPLETE_VERIFICATION_KEYS = INPUT_IDS.COMPLETE_VERIFICATION; -const bodyContent: Array> = [ConfirmAgreements]; +const PAGE_NAMES = CONST.BANK_ACCOUNT.PAGE_NAMES; +const SUB_PAGE_NAMES = CONST.BANK_ACCOUNT.COMPLETE_VERIFICATION_STEP.SUB_PAGE_NAMES; + +const pages = [{pageName: SUB_PAGE_NAMES.CONFIRM_AGREEMENTS, component: ConfirmAgreements}]; -function CompleteVerification({onBackButtonPress}: CompleteVerificationProps) { +function CompleteVerification({onBackButtonPress, onSubmit, backTo}: CompleteVerificationProps) { const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); @@ -31,6 +42,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 = useReimbursementAccountSubmit(onSubmit); const submit = useCallback(() => { acceptACHContractForBankAccount( @@ -43,21 +55,26 @@ function CompleteVerification({onBackButtonPress}: CompleteVerificationProps) { policyID, policyID ? lastPaymentMethod?.[policyID] : undefined, ); - }, [bankAccountID, values.isAuthorizedToUseBankAccount, values.certifyTrueInformation, values.acceptTermsAndConditions, policyID, lastPaymentMethod]); + markSubmitting(); + }, [bankAccountID, values.isAuthorizedToUseBankAccount, values.certifyTrueInformation, values.acceptTermsAndConditions, policyID, lastPaymentMethod, markSubmitting]); + + const buildRoute = useCallback( + (pageName: string, action?: 'edit') => ROUTES.BANK_ACCOUNT_USD_SETUP.getRoute({policyID, page: PAGE_NAMES.ACH_CONTRACT, subPage: pageName, action, backTo}), + [policyID, backTo], + ); - // eslint-disable-next-line @typescript-eslint/no-deprecated - const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo, goToTheLastStep} = useSubStep({bodyContent, startFrom: 0, onFinished: submit}); + const {CurrentPage, isEditing, pageIndex, nextPage, prevPage, moveTo} = useSubPage({pages, startFrom: 0, onFinished: submit, buildRoute}); const handleBackButtonPress = () => { if (isEditing) { - goToTheLastStep(); + Navigation.goBack(buildRoute(SUB_PAGE_NAMES.CONFIRM_AGREEMENTS)); return; } - if (screenIndex === 0) { + if (pageIndex === 0) { onBackButtonPress(); } else { - prevScreen(); + prevPage(); } }; @@ -71,9 +88,9 @@ function CompleteVerification({onBackButtonPress}: CompleteVerificationProps) { startStepIndex={6} stepNames={CONST.BANK_ACCOUNT.STEP_NAMES} > - diff --git a/src/pages/ReimbursementAccount/USD/CompleteVerification/subSteps/ConfirmAgreements.tsx b/src/pages/ReimbursementAccount/USD/CompleteVerification/subSteps/ConfirmAgreements.tsx index 0799cece14181..3e47077aa1890 100644 --- a/src/pages/ReimbursementAccount/USD/CompleteVerification/subSteps/ConfirmAgreements.tsx +++ b/src/pages/ReimbursementAccount/USD/CompleteVerification/subSteps/ConfirmAgreements.tsx @@ -7,15 +7,13 @@ 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 type {SubPageProps} from '@hooks/useSubPage/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,7 +36,7 @@ function TermsAndConditionsLabel() { return ; } -function ConfirmAgreements({onNext}: ConfirmAgreementsProps) { +function ConfirmAgreements({onNext}: SubPageProps) { const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const {translate} = useLocalize(); diff --git a/src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx b/src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx index 5d44b1b799497..491c737067c6c 100644 --- a/src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx +++ b/src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx @@ -1,6 +1,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import React from 'react'; import {View} from 'react-native'; +import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; @@ -8,10 +9,14 @@ 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 BankAccountValidationForm from './components/BankAccountValidationForm'; import FinishChatCard from './components/FinishChatCard'; @@ -21,15 +26,16 @@ 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; }; function ConnectBankAccount({onBackButtonPress, setShouldShowConnectedVerifiedBankAccount, setUSDBankAccountStep}: 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}`); @@ -45,6 +51,24 @@ function ConnectBankAccount({onBackButtonPress, setShouldShowConnectedVerifiedBa // 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 ( void; + setUSDBankAccountStep?: (step: string | null) => void; }; function FinishChatCard({requiresTwoFactorAuth, reimbursementAccount, setUSDBankAccountStep}: FinishChatCardProps) { @@ -78,7 +78,7 @@ 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); + setUSDBankAccountStep?.(CONST.BANK_ACCOUNT.STEP.REQUESTOR); goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR); }); }} diff --git a/src/pages/ReimbursementAccount/USD/Country/index.tsx b/src/pages/ReimbursementAccount/USD/Country/index.tsx index b4a28c913745d..ded09899c388f 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 378d04406214a..9f0a9057bbc0b 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 useReimbursementAccountSubmit from '@hooks/useReimbursementAccountSubmit'; +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 = useReimbursementAccountSubmit(onSubmit); const submit = useCallback( (isConfirmPage: boolean) => { updatePersonalInformationForBankAccount(bankAccountID, {...values}, policyID, isConfirmPage); @@ -48,27 +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, - // eslint-disable-next-line @typescript-eslint/no-deprecated - } = 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(); } }; @@ -83,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 cc63886621cec..39c912e0ce38e 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 17c29db3ce128..e9c1a8749b9c6 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 cde22e01ed979..54adf2a4be871 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 a1f8419637924..6093b3c2a77ab 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 ff9d07476a6a5..6e7384be05abc 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 4a9de8d65704a..f02b5186081d4 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 5947ef3f01f88..79a8cb8d70f3b 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, 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,35 @@ function VerifyIdentity({onBackButtonPress}: VerifyIdentityProps) { const policyID = reimbursementAccount?.achData?.policyID; const bankAccountID = reimbursementAccount?.achData?.bankAccountID; + // If Onfido is already complete (e.g. direct URL navigation), skip to next step + useEffect(() => { + if (!reimbursementAccount?.achData?.isOnfidoSetupComplete) { + return; + } + onSubmit?.(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- only check on mount + }, []); + 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 c13784998b2a5..0000000000000 --- 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 0000000000000..1d6e952904371 --- /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 0000000000000..c808a68bff8f4 --- /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 4dd8e53862689..33c3b64325184 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 ff3f23995a316..77aed07555935 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx @@ -18,7 +18,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getLastFourDigits} from '@libs/BankAccountUtils'; import {getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from '@libs/CardUtils'; 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'; @@ -66,7 +65,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 9e919333f8c46..11bafebd9a861 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx @@ -18,7 +18,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import {getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from '@libs/CardUtils'; 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'; @@ -69,7 +69,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 b54d7951346dc..f8e68e43f4b05 100644 --- a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx +++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx @@ -26,7 +26,7 @@ import {getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; 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, @@ -167,7 +167,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 1203dfce00b20..981ab69e16fff 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/tests/unit/ReimbursementAccountUtilsTest.ts b/tests/unit/ReimbursementAccountUtilsTest.ts index b531357e98656..e2f3be2e87031 100644 --- a/tests/unit/ReimbursementAccountUtilsTest.ts +++ b/tests/unit/ReimbursementAccountUtilsTest.ts @@ -1,19 +1,12 @@ import Onyx from 'react-native-onyx'; import CONST from '../../src/CONST'; -import {getBankAccountIDAsNumber, getRouteForCurrentStep, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '../../src/libs/ReimbursementAccountUtils'; +import {getBankAccountIDAsNumber} from '../../src/libs/ReimbursementAccountUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; import type {ACHDataReimbursementAccount} from '../../src/types/onyx/ReimbursementAccount'; Onyx.init({keys: ONYXKEYS}); describe('ReimbursementAccountUtils', () => { - 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