diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bf4ed40ec6f39..43ca7e65953e7 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -60,6 +60,7 @@ const ROUTES = { return `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const; }, }, + SEARCH_ROOT_VERIFY_ACCOUNT: `search/${VERIFY_ACCOUNT}`, SEARCH_SAVED_SEARCH_RENAME: { route: 'search/saved-search/rename', getRoute: ({name, jsonQuery}: {name: string; jsonQuery: SearchQueryString}) => `search/saved-search/rename?name=${name}&q=${jsonQuery}` as const, @@ -83,6 +84,10 @@ const ROUTES = { return getUrlWithBackToParam(baseRoute, backTo); }, }, + SEARCH_REPORT_VERIFY_ACCOUNT: { + route: `search/view/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `search/view/${reportID}/${VERIFY_ACCOUNT}` as const, + }, SEARCH_MONEY_REQUEST_REPORT: { route: 'search/r/:reportID', getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => { @@ -92,6 +97,10 @@ const ROUTES = { return getUrlWithBackToParam(baseRoute, backTo); }, }, + SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT: { + route: `search/r/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `search/r/${reportID}/${VERIFY_ACCOUNT}` as const, + }, SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS: { route: 'search/r/:reportID/hold', getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => { @@ -537,6 +546,10 @@ const ROUTES = { return getUrlWithBackToParam(`r/${reportID}/details/shareCode` as const, backTo); }, }, + REPORT_VERIFY_ACCOUNT: { + route: `r/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `r/${reportID}/${VERIFY_ACCOUNT}` as const, + }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -735,8 +748,12 @@ const ROUTES = { }, MONEY_REQUEST_CREATE: { route: ':action/:iouType/start/:transactionID/:reportID/:backToReport?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => - `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${backToReport ?? ''}` as const, + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => { + if (backToReport) { + return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/${backToReport}` as const; + } + return `${action as string}/${iouType as string}/start/${transactionID}/${reportID}` as const; + }, }, MONEY_REQUEST_STEP_SEND_FROM: { route: 'create/:iouType/from/:transactionID/:reportID', @@ -752,12 +769,22 @@ const ROUTES = { }, MONEY_REQUEST_STEP_CONFIRMATION: { route: ':action/:iouType/confirmation/:transactionID/:reportID/:backToReport?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backToReport?: string, participantsAutoAssigned?: boolean, backTo?: string) => + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backToReport?: string, participantsAutoAssigned?: boolean, backTo?: string) => { + let optionalRoutePart = ''; + if (backToReport !== undefined) { + optionalRoutePart += `/${backToReport}`; + } + if (participantsAutoAssigned !== undefined) { + optionalRoutePart += '?participantsAutoAssigned=true'; + } // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getUrlWithBackToParam( - `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}/${backToReport ?? ''}${participantsAutoAssigned ? '?participantsAutoAssigned=true' : ''}`, - backTo, - ), + return getUrlWithBackToParam(`${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}${optionalRoutePart}` as const, backTo); + }, + }, + MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT: { + route: `:action/:iouType/confirmation/:transactionID/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => + `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}/${VERIFY_ACCOUNT}` as const, }, MONEY_REQUEST_STEP_AMOUNT: { route: ':action/:iouType/amount/:transactionID/:reportID/:reportActionID?/:pageIndex?/:backToReport?', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5811bf7e08f62..f5fbfb7e65c64 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -41,9 +41,12 @@ const SCREENS = { }, SEARCH: { ROOT: 'Search_Root', + ROOT_VERIFY_ACCOUNT: 'Search_Root_Verify_Account', MONEY_REQUEST_REPORT: 'Search_Money_Request_Report', + MONEY_REQUEST_REPORT_VERIFY_ACCOUNT: 'Search_Money_Request_Report_Verify_Account', MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS: 'Search_Money_Request_Report_Hold_Transactions', REPORT_RHP: 'Search_Report_RHP', + REPORT_VERIFY_ACCOUNT: 'Search_Report_Verify_Account', ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP', ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP', ADVANCED_FILTERS_GROUP_BY_RHP: 'Search_Advanced_Filters_GroupBy_RHP', @@ -253,6 +256,7 @@ const SCREENS = { ADD_UNREPORTED_EXPENSE: 'AddUnreportedExpense', SCHEDULE_CALL: 'ScheduleCall', REPORT_CHANGE_APPROVER: 'Report_Change_Approver', + REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account', MERGE_TRANSACTION: 'MergeTransaction', }, PUBLIC_CONSOLE_DEBUG: 'Console_Debug', @@ -268,6 +272,7 @@ const SCREENS = { HOLD: 'Money_Request_Hold_Reason', REJECT: 'Money_Request_Reject_Reason', STEP_CONFIRMATION: 'Money_Request_Step_Confirmation', + STEP_CONFIRMATION_VERIFY_ACCOUNT: 'Money_Request_Step_Confirmation_Verify_Account', START: 'Money_Request_Start', STEP_UPGRADE: 'Money_Request_Step_Upgrade', STEP_AMOUNT: 'Money_Request_Step_Amount', @@ -783,6 +788,7 @@ const SCREENS = { REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount', REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO: 'Reimbursement_Account_Signer_Info', REFERRAL_DETAILS: 'Referral_Details', + REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account', KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', SHARE: { ROOT: 'Share_Root', diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index b3b2823f0482e..023ec0190e440 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -34,6 +34,7 @@ import { isInvoiceReport as isInvoiceReportUtil, isIOUReport, } from '@libs/ReportUtils'; +import {getSettlementButtonPaymentMethods, handleUnvalidatedUserNavigation} from '@libs/SettlementButtonUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {setPersonalBankAccountContinueKYCOnSuccess} from '@userActions/BankAccounts'; import {approveMoneyRequest} from '@userActions/IOU'; @@ -179,7 +180,7 @@ function SettlementButton({ } if (!isUserValidated) { - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute())); + handleUnvalidatedUserNavigation(chatReportID, reportID); return true; } @@ -189,7 +190,7 @@ function SettlementButton({ } return false; - }, [isAccountLocked, isUserValidated, policy, showLockedAccountModal]); + }, [policy, isAccountLocked, isUserValidated, chatReportID, reportID, showLockedAccountModal]); const getPaymentSubitems = useCallback( (payAsBusiness: boolean) => { @@ -225,24 +226,7 @@ function SettlementButton({ const paymentButtonOptions = useMemo(() => { const buttonOptions = []; - const paymentMethods = { - [CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]: { - text: hasActivatedWallet ? translate('iou.settleWallet', {formattedAmount: ''}) : translate('iou.settlePersonal', {formattedAmount: ''}), - icon: Expensicons.User, - value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, - }, - [CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]: { - text: translate('iou.settleBusiness', {formattedAmount: ''}), - icon: Expensicons.Building, - value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, - }, - [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: Expensicons.CheckCircle, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - shouldUpdateSelectedIndex: false, - }, - }; + const paymentMethods = getSettlementButtonPaymentMethods(hasActivatedWallet, translate); const shortFormPayElsewhereButton = { text: translate('iou.pay'), diff --git a/src/hooks/useBulkPayOptions.ts b/src/hooks/useBulkPayOptions.ts index 11df01d894dea..12368653e3590 100644 --- a/src/hooks/useBulkPayOptions.ts +++ b/src/hooks/useBulkPayOptions.ts @@ -1,11 +1,12 @@ import truncate from 'lodash/truncate'; import {useMemo} from 'react'; -import {Bank, Building, CheckCircle, User, Wallet} from '@components/Icon/Expensicons'; +import {Bank, Building, Wallet} from '@components/Icon/Expensicons'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import type {BankAccountMenuItem} from '@components/Search/types'; import {formatPaymentMethods} from '@libs/PaymentUtils'; import {hasRequestFromCurrentAccount} from '@libs/ReportActionsUtils'; import {isExpenseReport as isExpenseReportUtil, isInvoiceReport as isInvoiceReportUtil, isIOUReport as isIOUReportUtil} from '@libs/ReportUtils'; +import {getSettlementButtonPaymentMethods} from '@libs/SettlementButtonUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {AccountData, Policy} from '@src/types/onyx'; @@ -81,23 +82,7 @@ function useBulkPayOptions({selectedPolicyID, selectedReportID, activeAdminPolic const bulkPayButtonOptions = useMemo(() => { const buttonOptions = []; - const paymentMethods = { - [CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]: { - text: hasActivatedWallet ? translate('iou.settleWallet', {formattedAmount: ''}) : translate('iou.settlePersonal', {formattedAmount: ''}), - icon: User, - key: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, - }, - [CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]: { - text: translate('iou.settleBusiness', {formattedAmount: ''}), - icon: Building, - key: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, - }, - [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: CheckCircle, - key: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - }, - }; + const paymentMethods = getSettlementButtonPaymentMethods(hasActivatedWallet, translate); if (!selectedReportID || !selectedPolicyID) { return undefined; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index bdc9bebdbf545..e2ad1fd1d7cbd 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -32,6 +32,7 @@ import type { ReportDescriptionNavigatorParamList, ReportDetailsNavigatorParamList, ReportSettingsNavigatorParamList, + ReportVerifyAccountNavigatorParamList, RoomMembersNavigatorParamList, ScheduleCallParamList, SearchAdvancedFiltersParamList, @@ -144,6 +145,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/IOURequestRedirectToStartPage').default, [SCREENS.MONEY_REQUEST.CREATE]: () => require('../../../../pages/iou/request/IOURequestStartPage').default, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: () => require('../../../../pages/iou/request/step/IOURequestStepConfirmation').default, + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: () => require('../../../../pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage').default, [SCREENS.MONEY_REQUEST.STEP_AMOUNT]: () => require('../../../../pages/iou/request/step/IOURequestStepAmount').default, [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: () => require('../../../../pages/iou/request/step/IOURequestStepTaxAmountPage').default, [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: () => require('../../../../pages/iou/request/step/IOURequestStepTaxRatePage').default, @@ -255,6 +257,11 @@ const TaskModalStackNavigator = createModalStackNavigator require('../../../../pages/tasks/TaskAssigneeSelectorModal').default, }); +const ReportVerifyAccountModalStackNavigator = createModalStackNavigator({ + [SCREENS.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/home/report/ReportVerifyAccountPage').default, + [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchReportVerifyAccountPage').default, +}); + const ReportDescriptionModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_DESCRIPTION_ROOT]: () => require('../../../../pages/ReportDescriptionPage').default, }); @@ -832,6 +839,8 @@ const MergeTransactionStackNavigator = createModalStackNavigator({ [SCREENS.SEARCH.REPORT_RHP]: () => require('../../../../pages/home/ReportScreen').default, + [SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchRootVerifyAccountPage').default, + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchMoneyRequestReportVerifyAccountPage').default, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: () => require('../../../../pages/Search/SearchHoldReasonPage').default, [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: () => require('../../../../pages/Search/SearchHoldReasonPage').default, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: () => require('../../../../pages/Search/SearchTransactionsChangeReport').default, @@ -950,6 +959,7 @@ export { DomainCardModalStackNavigator, SplitDetailsModalStackNavigator, TaskModalStackNavigator, + ReportVerifyAccountModalStackNavigator, WalletStatementStackNavigator, TransactionDuplicateStackNavigator, SearchReportModalStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 6c6e525903b37..ef90320d3f125 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -131,6 +131,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION} component={ModalStackNavigators.ReportDescriptionModalStackNavigator} /> + > = { [SCREENS.SEARCH.ROOT]: [ + SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT, SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_GROUP_BY_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP, SCREENS.SEARCH.REPORT_RHP, + SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT, SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP, SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP, SCREENS.SEARCH.ADVANCED_FILTERS_RHP, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index ca0303aba72f8..7dd30c6405c64 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1392,6 +1392,7 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: ROUTES.MONEY_REQUEST_STEP_CATEGORY.route, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.route, + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.route, [SCREENS.MONEY_REQUEST.STEP_CURRENCY]: ROUTES.MONEY_REQUEST_STEP_CURRENCY.route, [SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route, @@ -1530,6 +1531,12 @@ const config: LinkingOptions['config'] = { [SCREENS.REFERRAL_DETAILS]: ROUTES.REFERRAL_DETAILS_MODAL.route, }, }, + [SCREENS.RIGHT_MODAL.REPORT_VERIFY_ACCOUNT]: { + screens: { + [SCREENS.REPORT_VERIFY_ACCOUNT]: ROUTES.REPORT_VERIFY_ACCOUNT.route, + [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.route, + }, + }, [SCREENS.RIGHT_MODAL.TRAVEL]: { screens: { [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, @@ -1552,7 +1559,9 @@ const config: LinkingOptions['config'] = { }, [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { screens: { + [SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT]: ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT, [SCREENS.SEARCH.REPORT_RHP]: ROUTES.SEARCH_REPORT.route, + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.route, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS.route, [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 873d60e713db2..0bc10799a12cd 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -83,6 +83,12 @@ type ConsoleNavigatorParamList = { }; }; +type ReportVerifyAccountNavigatorParamList = { + [SCREENS.REPORT_VERIFY_ACCOUNT]: { + reportID: string; + }; +}; + type SettingsNavigatorParamList = { [SCREENS.SETTINGS.SHARE_CODE]: undefined; [SCREENS.SETTINGS.PROFILE.PRONOUNS]: undefined; @@ -1673,6 +1679,12 @@ type MoneyRequestNavigatorParamList = { participantsAutoAssigned?: string; backToReport?: string; }; + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: { + action: IOUAction; + iouType: IOUType; + transactionID: string; + reportID: string; + }; [SCREENS.MONEY_REQUEST.STEP_SCAN]: { action: IOUAction; iouType: IOUType; @@ -2051,6 +2063,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams; [SCREENS.SETTINGS.SHARE_CODE]: undefined; + [SCREENS.RIGHT_MODAL.REPORT_VERIFY_ACCOUNT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.NEW_REPORT_WORKSPACE_SELECTION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.SET_DEFAULT_WORKSPACE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams; @@ -2550,6 +2563,12 @@ type SearchReportParamList = { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; }; + [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: { + reportID: string; + }; + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: { + reportID: string; + }; [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: { /** ID of the transaction the page was opened for */ transactionID: string; @@ -2743,6 +2762,7 @@ export type { ProfileNavigatorParamList, PublicScreensParamList, ReferralDetailsNavigatorParamList, + ReportVerifyAccountNavigatorParamList, ReimbursementAccountNavigatorParamList, ReimbursementAccountEnterSignerInfoNavigatorParamList, NewReportWorkspaceSelectionNavigatorParamList, diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts new file mode 100644 index 0000000000000..c873e8216d951 --- /dev/null +++ b/src/libs/SettlementButtonUtils.ts @@ -0,0 +1,103 @@ +import * as Expensicons from '@components/Icon/Expensicons'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import Log from './Log'; +import Navigation from './Navigation/Navigation'; + +type RouteMapping = { + /** Condition that determines if this route mapping applies to the current active route */ + check: (activeRoute: string) => boolean; + + /** Navigates to the appropriate verification route when the check condition is met */ + navigate: () => void; +}; + +/** + * Retrieves an array of available RouteMappings for an unvalidated user. + * Each mapping contains a `check` function that determines whether the activeRoute matches the given mapping and a `navigate` function that executes navigation to the corresponding route. + * @param chatReportID - The chat or workspace ID from which the unvalidated user makes a payment via SettlementButton + * @param reportID - The expense report ID that the user pays using SettlementButton (optional) + * @return An array of available RouteMappings suitable for an unvalidated user + */ +const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping[] => { + const nonReportIdRouteMappings = [ + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_ROOT.getRoute({query: ''})), + navigate: () => Navigation.navigate(ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT), + }, + { + check: (activeRoute: string) => + activeRoute.includes(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID)), + navigate: () => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, chatReportID), + ), + }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(chatReportID)), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), + }, + ]; + + if (reportID === undefined) { + return nonReportIdRouteMappings; + } + + const reportIdRouteMappings = [ + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID})), + navigate: () => Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID})), + navigate: () => Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID)), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, + ]; + + return [...nonReportIdRouteMappings, ...reportIdRouteMappings]; +}; + +/** + * Handles SettlementButton navigation for unvalidated users based on the active route and current chatID, reportID (optional). + */ +const handleUnvalidatedUserNavigation = (chatReportID: string, reportID?: string) => { + const activeRoute = Navigation.getActiveRoute(); + const matchedRoute = getRouteMappings(chatReportID, reportID).find((mapping) => mapping.check(activeRoute)); + + if (matchedRoute) { + matchedRoute.navigate(); + return; + } + Log.warn('Failed to navigate to the correct path'); +}; + +/** + * Retrieves SettlementButton payment methods. + */ +const getSettlementButtonPaymentMethods = (hasActivatedWallet: boolean, translate: LocaleContextProps['translate']) => { + return { + [CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]: { + text: hasActivatedWallet ? translate('iou.settleWallet', {formattedAmount: ''}) : translate('iou.settlePersonal', {formattedAmount: ''}), + icon: Expensicons.User, + value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + }, + [CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]: { + text: translate('iou.settleBusiness', {formattedAmount: ''}), + icon: Expensicons.Building, + value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, + }, + [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.CheckCircle, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + shouldUpdateSelectedIndex: false, + }, + }; +}; + +export {handleUnvalidatedUserNavigation, getSettlementButtonPaymentMethods}; diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index 8f483429c35e3..68c7345c6d18c 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -15,7 +15,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; -function SearchHoldReasonPage({route}: PlatformStackScreenProps>) { +type SearchHoldReasonPageProps = + | PlatformStackScreenProps + | PlatformStackScreenProps; + +function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { const {translate} = useLocalize(); const {backTo = '', reportID} = route.params ?? {}; const context = useSearchContext(); diff --git a/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx b/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx new file mode 100644 index 0000000000000..241b04324f902 --- /dev/null +++ b/src/pages/Search/SearchMoneyRequestReportVerifyAccountPage.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SearchReportParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type SearchMoneyRequestReportVerifyAccountPageProps = PlatformStackScreenProps; + +function SearchMoneyRequestReportVerifyAccountPage({route}: SearchMoneyRequestReportVerifyAccountPageProps) { + return ; +} + +SearchMoneyRequestReportVerifyAccountPage.displayName = 'SearchMoneyRequestReportVerifyAccountPage'; + +export default SearchMoneyRequestReportVerifyAccountPage; diff --git a/src/pages/Search/SearchReportVerifyAccountPage.tsx b/src/pages/Search/SearchReportVerifyAccountPage.tsx new file mode 100644 index 0000000000000..820f674dc3b5b --- /dev/null +++ b/src/pages/Search/SearchReportVerifyAccountPage.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SearchReportParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type SearchReportVerifyAccountPageProps = PlatformStackScreenProps; + +function SearchReportVerifyAccountPage({route}: SearchReportVerifyAccountPageProps) { + return ; +} + +SearchReportVerifyAccountPage.displayName = 'SearchReportVerifyAccountPage'; + +export default SearchReportVerifyAccountPage; diff --git a/src/pages/Search/SearchRootVerifyAccountPage.tsx b/src/pages/Search/SearchRootVerifyAccountPage.tsx new file mode 100644 index 0000000000000..5d5ff472830e9 --- /dev/null +++ b/src/pages/Search/SearchRootVerifyAccountPage.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; + +function SearchRootVerifyAccountPage() { + return ; +} + +SearchRootVerifyAccountPage.displayName = 'SearchRootVerifyAccountPage'; + +export default SearchRootVerifyAccountPage; diff --git a/src/pages/home/report/ReportVerifyAccountPage.tsx b/src/pages/home/report/ReportVerifyAccountPage.tsx new file mode 100644 index 0000000000000..10c6f34cc6dc9 --- /dev/null +++ b/src/pages/home/report/ReportVerifyAccountPage.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {ReportVerifyAccountNavigatorParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ReportVerifyAccountPageProps = PlatformStackScreenProps; + +function ReportVerifyAccountPage({route}: ReportVerifyAccountPageProps) { + return ; +} + +ReportVerifyAccountPage.displayName = 'ReportVerifyAccountPage'; + +export default ReportVerifyAccountPage; diff --git a/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx b/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx new file mode 100644 index 0000000000000..0dcefabf56e0e --- /dev/null +++ b/src/pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type MoneyRequestStepConfirmationVerifyAccountPageProps = PlatformStackScreenProps; + +function MoneyRequestStepConfirmationVerifyAccountPage({route}: MoneyRequestStepConfirmationVerifyAccountPageProps) { + return ( + + ); +} + +MoneyRequestStepConfirmationVerifyAccountPage.displayName = 'MoneyRequestStepConfirmationVerifyAccountPage'; + +export default MoneyRequestStepConfirmationVerifyAccountPage; diff --git a/tests/unit/SettlementButtonUtilsTest.ts b/tests/unit/SettlementButtonUtilsTest.ts new file mode 100644 index 0000000000000..3afeb777f72fd --- /dev/null +++ b/tests/unit/SettlementButtonUtilsTest.ts @@ -0,0 +1,211 @@ +import * as Expensicons from '@components/Icon/Expensicons'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import {getSettlementButtonPaymentMethods, handleUnvalidatedUserNavigation} from '@libs/SettlementButtonUtils'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +jest.mock('@libs/Navigation/Navigation'); + +const mockTranslate = jest.fn((key: string) => key); + +jest.mock('@hooks/useLocalize', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: () => ({ + translate: mockTranslate, + }), +})); + +describe('handleUnvalidatedUserNavigation', () => { + const mockReportID = '123456789'; + const mockChatReportID = '987654321'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + // handleUnvalidatedUserNavigation navigates to the correct route + it.each([ + { + description: 'navigate to ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_ROOT', + mockActiveRoute: ROUTES.SEARCH_ROOT.getRoute({query: ''}), + expectedRouteToNavigate: ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT, + }, + { + description: 'navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_REPORT', + mockActiveRoute: ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}), + expectedRouteToNavigate: ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID), + }, + { + description: 'navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when active route is ROUTES.SEARCH_MONEY_REQUEST_REPORT', + mockActiveRoute: ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}), + expectedRouteToNavigate: ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID), + }, + { + description: 'navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(chatReportID)', + mockActiveRoute: ROUTES.REPORT_WITH_ID.getRoute(mockChatReportID), + expectedRouteToNavigate: ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockChatReportID), + }, + { + description: 'navigate to ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID) when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID)', + mockActiveRoute: ROUTES.REPORT_WITH_ID.getRoute(mockReportID), + expectedRouteToNavigate: ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID), + }, + { + description: 'navigate to ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT when active route is ROUTES.MONEY_REQUEST_STEP_CONFIRMATION', + mockActiveRoute: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.PAY, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, mockChatReportID), + expectedRouteToNavigate: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.getRoute( + CONST.IOU.ACTION.CREATE, + CONST.IOU.TYPE.PAY, + CONST.IOU.OPTIMISTIC_TRANSACTION_ID, + mockChatReportID, + ), + }, + ])('$description', ({mockActiveRoute, expectedRouteToNavigate}) => { + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); + expect(Navigation.navigate).toHaveBeenCalledWith(expectedRouteToNavigate); + expect(Navigation.navigate).toHaveBeenCalledTimes(1); + }); + + // handleUnvalidatedUserNavigation does not navigate to the route that require reportID, when reportID is undefined + it.each([ + { + description: 'do not navigate to ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT when reportID is undefined', + mockActiveRoute: ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}), + }, + { + description: 'do not navigate to ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT when reportID is undefined', + mockActiveRoute: ROUTES.SEARCH_REPORT.getRoute({reportID: mockReportID}), + }, + { + description: 'do not navigate when active route is ROUTES.REPORT_WITH_ID.getRoute(reportID) and reportID is undefined', + mockActiveRoute: ROUTES.REPORT_WITH_ID.getRoute(mockReportID), + }, + ])('$description', ({mockActiveRoute}) => { + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockChatReportID); + expect(Navigation.navigate).not.toHaveBeenCalled(); + }); + + // handleUnvalidatedUserNavigation matches the first applicable route when multiple conditions could match + it('match ROUTES.SEARCH_MONEY_REQUEST_REPORT over ROUTES.REPORT_WITH_ID', () => { + const mockActiveRoute = ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: mockReportID}); + (Navigation.getActiveRoute as jest.Mock).mockReturnValue(mockActiveRoute); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); + expect(Navigation.navigate).toHaveBeenCalledTimes(1); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + expect(Navigation.navigate).not.toHaveBeenCalledWith(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(mockReportID)); + }); + + // handleUnvalidatedUserNavigation does not navigate when no route mapping matches + it('when no route mapping matches, user should not be navigated', () => { + (Navigation.getActiveRoute as jest.Mock).mockReturnValue('/just/unmatched/route'); + handleUnvalidatedUserNavigation(mockChatReportID, mockReportID); + expect(Navigation.navigate).not.toHaveBeenCalled(); + }); +}); + +describe('getSettlementButtonPaymentMethods', () => { + const {translate} = useLocalize(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return payment method with wallet for PERSONAL_BANK_ACCOUNT when hasActivatedWallet is true', () => { + const result = getSettlementButtonPaymentMethods(true, translate); + expect(result[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]).toEqual({ + text: translate('iou.settleWallet', {formattedAmount: ''}), + icon: Expensicons.User, + value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + }); + }); + + it('should return payment method with personal bank account for PERSONAL_BANK_ACCOUNT when hasActivatedWallet is false', () => { + const result = getSettlementButtonPaymentMethods(false, translate); + expect(result[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]).toEqual({ + text: translate('iou.settlePersonal', {formattedAmount: ''}), + icon: Expensicons.User, + value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + }); + }); + + it('should return payment method with business bank account for BUSINESS_BANK_ACCOUNT', () => { + const result = getSettlementButtonPaymentMethods(true, translate); + expect(result[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]).toEqual({ + text: translate('iou.settleBusiness', {formattedAmount: ''}), + icon: Expensicons.Building, + value: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, + }); + }); + + it('should return payment method elsewhere for ELSEWHERE', () => { + const result = getSettlementButtonPaymentMethods(true, translate); + expect(result[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]).toEqual({ + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.CheckCircle, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + shouldUpdateSelectedIndex: false, + }); + }); + + it('should return all three payment methods', () => { + const result = getSettlementButtonPaymentMethods(true, translate); + expect(Object.keys(result)).toHaveLength(3); + expect(result).toHaveProperty(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); + expect(result).toHaveProperty(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT); + expect(result).toHaveProperty(CONST.IOU.PAYMENT_TYPE.ELSEWHERE); + }); + + it.each([ + {hasActivatedWallet: true, expectedPersonalKey: 'iou.settleWallet'}, + {hasActivatedWallet: false, expectedPersonalKey: 'iou.settlePersonal'}, + ])('should use correct texts for each payment method when hasActivatedWallet is $hasActivatedWallet', ({hasActivatedWallet, expectedPersonalKey}) => { + const result = getSettlementButtonPaymentMethods(hasActivatedWallet, translate); + expect(translate).toHaveBeenCalledTimes(3); + expect(translate).toHaveBeenCalledWith(expectedPersonalKey, {formattedAmount: ''}); + expect(translate).toHaveBeenCalledWith('iou.settleBusiness', {formattedAmount: ''}); + expect(translate).toHaveBeenCalledWith('iou.payElsewhere', {formattedAmount: ''}); + expect(result[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT].text).toBe(expectedPersonalKey); + expect(result[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT].text).toBe('iou.settleBusiness'); + expect(result[CONST.IOU.PAYMENT_TYPE.ELSEWHERE].text).toBe('iou.payElsewhere'); + }); + + it.each([ + { + method: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + expectedIcon: Expensicons.User, + expectedValue: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + description: 'PERSONAL_BANK_ACCOUNT', + }, + { + method: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, + expectedIcon: Expensicons.Building, + expectedValue: CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, + description: 'BUSINESS_BANK_ACCOUNT', + }, + { + method: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + expectedIcon: Expensicons.CheckCircle, + expectedValue: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + description: 'ELSEWHERE', + }, + ])('should use correct icon and value for $description regardless of hasActivatedWallet', ({method, expectedIcon, expectedValue}) => { + const resultWithWallet = getSettlementButtonPaymentMethods(true, translate); + const resultWithoutWallet = getSettlementButtonPaymentMethods(false, translate); + [resultWithWallet, resultWithoutWallet].forEach((result) => { + const paymentMethod = result[method]; + expect(paymentMethod.icon).toBe(expectedIcon); + expect(paymentMethod.value).toBe(expectedValue); + }); + }); + + it('should only set shouldUpdateSelectedIndex for elsewhere payment type', () => { + const result = getSettlementButtonPaymentMethods(true, translate); + expect(result[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]).not.toHaveProperty('shouldUpdateSelectedIndex'); + expect(result[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]).not.toHaveProperty('shouldUpdateSelectedIndex'); + expect(result[CONST.IOU.PAYMENT_TYPE.ELSEWHERE].shouldUpdateSelectedIndex).toBe(false); + }); +});