diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 46b60effdd9c..8fc7c1970c2d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1519,6 +1519,10 @@ const ROUTES = { return `workspaces/${policyID}/workflows` as const; }, }, + WORKSPACE_WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT: { + route: 'workspaces/:policyID/workflows/connect-account', + getRoute: (policyID: string) => `workspaces/${policyID}/workflows/connect-account` as const, + }, WORKSPACE_WORKFLOWS_APPROVALS_NEW: { route: 'workspaces/:policyID/workflows/approvals/new', getRoute: (policyID: string) => `workspaces/${policyID}/workflows/approvals/new` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 60c331502b72..df1c06c86a67 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -616,6 +616,7 @@ const SCREENS = { WORKFLOWS_APPROVALS_APPROVER: 'Workspace_Workflows_Approvals_Approver', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', + WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT: 'Workspace_Workflows_Connect_Existing_Bank_Account', DESCRIPTION: 'Workspace_Overview_Description', SHARE: 'Workspace_Overview_Share', NAME: 'Workspace_Overview_Name', diff --git a/src/components/AddToWalletButton/index.native.tsx b/src/components/AddToWalletButton/index.native.tsx index 6e18ec8d579a..a6f3b51735d2 100644 --- a/src/components/AddToWalletButton/index.native.tsx +++ b/src/components/AddToWalletButton/index.native.tsx @@ -6,7 +6,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {openWalletPage} from '@libs/actions/PaymentMethods'; +import {getPaymentMethods} from '@libs/actions/PaymentMethods'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import {checkIfWalletIsAvailable, handleAddCardToWallet, isCardInWallet} from '@libs/Wallet/index'; @@ -42,7 +42,7 @@ function AddToWalletButton({card, cardHolderName, cardDescription, buttonStyle}: .then((status: TokenizationStatus) => { if (status === 'success') { Log.info('Card added to wallet'); - openWalletPage(); + getPaymentMethods(); } else { setIsLoading(false); } diff --git a/src/languages/de.ts b/src/languages/de.ts index 961a86e6bab2..1da1c7acc828 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5437,6 +5437,7 @@ const translations = { updateWorkspaceCurrency: 'Arbeitsbereichswährung aktualisieren', workspaceCurrencyNotSupported: 'Arbeitsbereichswährung wird nicht unterstützt', yourWorkspace: `Ihr Arbeitsbereich ist auf eine nicht unterstützte Währung eingestellt. Sehen Sie sich die Liste der unterstützten Währungen an.`, + chooseAnExisting: 'Wählen Sie ein vorhandenes Bankkonto zur Zahlung von Ausgaben oder fügen Sie ein neues hinzu.', }, changeOwner: { changeOwnerPageTitle: 'Besitzer übertragen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 70e16c7f0607..2924e6b4ac06 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5411,6 +5411,7 @@ const translations = { updateWorkspaceCurrency: 'Update workspace currency', workspaceCurrencyNotSupported: 'Workspace currency not supported', yourWorkspace: `Your workspace is set to an unsupported currency. View the list of supported currencies.`, + chooseAnExisting: 'Choose an existing bank account to pay expenses or add a new one.', }, changeOwner: { changeOwnerPageTitle: 'Transfer owner', diff --git a/src/languages/es.ts b/src/languages/es.ts index dad2ab01237d..ab9fedb35321 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5425,6 +5425,7 @@ const translations = { updateWorkspaceCurrency: 'Actualizar la moneda del espacio de trabajo', workspaceCurrencyNotSupported: 'Moneda del espacio de trabajo no soportada', yourWorkspace: `Tu espacio de trabajo está configurado en una moneda no soportada. Consulta la lista de monedas soportadas.`, + chooseAnExisting: 'Elige una cuenta bancaria existente para pagar gastos o añade una nueva.', }, changeOwner: { changeOwnerPageTitle: 'Transferir la propiedad', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 914eba731fe9..e0056e0f5ab7 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5449,6 +5449,7 @@ const translations = { updateWorkspaceCurrency: "Mettre à jour la devise de l'espace de travail", workspaceCurrencyNotSupported: "Devise de l'espace de travail non prise en charge", yourWorkspace: `Votre espace de travail est configuré avec une devise non prise en charge. Consultez la liste des devises prises en charge.`, + chooseAnExisting: 'Choisissez un compte bancaire existant pour payer les dépenses ou ajoutez-en un nouveau.', }, changeOwner: { changeOwnerPageTitle: 'Transférer le propriétaire', diff --git a/src/languages/it.ts b/src/languages/it.ts index 07b3cf5eeedc..149d891137ab 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5446,6 +5446,7 @@ const translations = { updateWorkspaceCurrency: 'Aggiorna la valuta dello spazio di lavoro', workspaceCurrencyNotSupported: "Valuta dell'area di lavoro non supportata", yourWorkspace: `Il tuo spazio di lavoro è impostato su una valuta non supportata. Visualizza l'elenco delle valute supportate.`, + chooseAnExisting: 'Scegli un conto bancario esistente per pagare le spese o aggiungine uno nuovo.', }, changeOwner: { changeOwnerPageTitle: 'Trasferisci proprietario', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 6971de44e693..9577f35c4e90 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5416,6 +5416,7 @@ const translations = { updateWorkspaceCurrency: 'ワークスペースの通貨を更新する', workspaceCurrencyNotSupported: 'ワークスペース通貨はサポートされていません', yourWorkspace: `ご使用のワークスペースは、サポートされていない通貨に設定されています。サポートされている通貨の一覧をご確認ください。`, + chooseAnExisting: '既存の銀行口座を選択して経費を支払うか、新しい口座を追加してください。', }, changeOwner: { changeOwnerPageTitle: 'オーナーを移行', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 336672ecaa13..5cb9033e872e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5444,6 +5444,7 @@ const translations = { updateWorkspaceCurrency: 'Werkruimte valuta bijwerken', workspaceCurrencyNotSupported: 'Werkruimtevaluta niet ondersteund', yourWorkspace: `Uw werkruimte is ingesteld op een niet-ondersteunde valuta. Bekijk de lijst met ondersteunde valuta's.`, + chooseAnExisting: 'Kies een bestaande bankrekening om uitgaven te betalen of voeg een nieuwe toe.', }, changeOwner: { changeOwnerPageTitle: 'Eigenaar overdragen', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 0e3e537c3ab7..25eb810b6ef5 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5432,6 +5432,7 @@ const translations = { updateWorkspaceCurrency: 'Zaktualizuj walutę przestrzeni roboczej', workspaceCurrencyNotSupported: 'Waluta przestrzeni roboczej nie jest obsługiwana', yourWorkspace: `Twoje miejsce pracy jest ustawione na nieobsługiwaną walutę. Zobacz listę obsługiwanych walut.`, + chooseAnExisting: 'Wybierz istniejące konto bankowe do płacenia wydatków lub dodaj nowe.', }, changeOwner: { changeOwnerPageTitle: 'Przenieś właściciela', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index f2e632160d5c..389fd1b480ca 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5443,6 +5443,7 @@ const translations = { updateWorkspaceCurrency: 'Atualizar moeda do espaço de trabalho', workspaceCurrencyNotSupported: 'Moeda do espaço de trabalho não suportada', yourWorkspace: `Seu espaço de trabalho está configurado para uma moeda não suportada. Veja a lista de moedas suportadas.`, + chooseAnExisting: 'Escolha uma conta bancária existente para pagar despesas ou adicione uma nova.', }, changeOwner: { changeOwnerPageTitle: 'Transferir proprietário', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 7206c8021907..440f95fe135a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5347,6 +5347,7 @@ const translations = { updateWorkspaceCurrency: '更新工作区货币', workspaceCurrencyNotSupported: '工作区货币不支持', yourWorkspace: `您的工作区设置为不支持的货币。查看支持货币列表。`, + chooseAnExisting: '选择现有银行账户支付费用或添加新账户。', }, changeOwner: { changeOwnerPageTitle: '转移所有者', diff --git a/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts b/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts index f96f6385f541..1d9178388aab 100644 --- a/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts +++ b/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts @@ -4,6 +4,7 @@ import type CONST from '@src/CONST'; type SetWorkspaceReimbursementParams = { policyID: string; reimbursementChoice: ValueOf; + bankAccountID?: number; }; export default SetWorkspaceReimbursementParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index ac29d78489b1..d9b74656e0a7 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -499,6 +499,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersDisplayedAsPage').default, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_DESKTOP_ITEMS]: () => require('../../../../pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage').default, + [SCREENS.WORKSPACE.WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT]: () => + require('../../../../pages/workspace/workflows/WorkspaceWorkflowsConnectExistingBankAccountPage').default, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../../pages/ReimbursementAccount/ReimbursementAccountPage').default, [SCREENS.REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO]: () => require('../../../../pages/ReimbursementAccount/EnterSignerInfo').default, [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: () => require('../../../../pages/settings/Wallet/ReportCardLostPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 32e2b8da7bdb..fb664d38905a 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -33,6 +33,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: { path: ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.route, }, + [SCREENS.WORKSPACE.WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT]: { + path: ROUTES.WORKSPACE_WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT.route, + exact: true, + }, [SCREENS.REIMBURSEMENT_ACCOUNT]: { path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index faf002f227ee..4cc33ba9ce94 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1969,6 +1969,9 @@ type WorkspaceSplitNavigatorParamList = { [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: { policyID: string; }; + [SCREENS.WORKSPACE.WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT]: { + policyID: string; + }; [SCREENS.WORKSPACE.INVOICES]: { policyID: string; }; diff --git a/src/libs/ReimbursementAccountUtils.ts b/src/libs/ReimbursementAccountUtils.ts index 9bd7c049e6f7..b388c3389a04 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 {ReimbursementAccountStep} from '@src/types/onyx/ReimbursementAccount'; +import type {ACHDataReimbursementAccount, ReimbursementAccountStep} from '@src/types/onyx/ReimbursementAccount'; type ReimbursementAccountStepToOpen = ValueOf | ''; @@ -35,5 +35,26 @@ function getRouteForCurrentStep(currentStep: ReimbursementAccountStep): Reimburs } } -export {getRouteForCurrentStep, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES}; +/** + * Returns true if a VBBA exists in any state other than OPEN or LOCKED + */ +const hasInProgressUSDVBBA = (achData?: ACHDataReimbursementAccount): boolean => { + return !!achData?.bankAccountID && !!achData?.state && achData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN && achData?.state !== CONST.BANK_ACCOUNT.STATE.LOCKED; +}; + +/** Returns true if user passed first step of flow for non USD VBBA */ +const hasInProgressNonUSDVBBA = (achData?: ACHDataReimbursementAccount, nonUSDCountryDraftValue?: string): boolean => { + return (!!achData?.bankAccountID && !!achData?.created) || nonUSDCountryDraftValue !== ''; +}; + +/** Returns true if VBBA flow is in progress */ +const hasInProgressVBBA = (achData?: ACHDataReimbursementAccount, isNonUSDWorkspace?: boolean, nonUSDCountryDraftValue?: string) => { + if (isNonUSDWorkspace) { + return hasInProgressNonUSDVBBA(achData, nonUSDCountryDraftValue); + } + + return hasInProgressUSDVBBA(achData); +}; + +export {getRouteForCurrentStep, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES, hasInProgressUSDVBBA, hasInProgressNonUSDVBBA, hasInProgressVBBA}; export type {ReimbursementAccountStepToOpen}; diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 743e87e87d81..a22d87eb717f 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -3,6 +3,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; +import type {BankAccountList} from '@src/types/onyx'; import type {ApprovalWorkflowOnyx, Approver, Member} from '@src/types/onyx/ApprovalWorkflow'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; import type {PersonalDetailsList} from '@src/types/onyx/PersonalDetails'; @@ -304,6 +305,7 @@ function convertApprovalWorkflowToPolicyEmployees({ return updatedEmployeeList; } + function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover, ownerDetails}: UpdateWorkflowDataOnApproverRemovalParams): UpdateWorkflowDataOnApproverRemovalResult { const defaultWorkflow = approvalWorkflows.find((workflow) => workflow.isDefault); const removedApproverEmail = removedApprover.login; @@ -414,4 +416,24 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover }); } -export {calculateApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, INITIAL_APPROVAL_WORKFLOW, updateWorkflowDataOnApproverRemoval}; +/** + * Get eligible business bank accounts for the workspace reimbursement workflow + */ +function getEligibleExistingBusinessBankAccounts(bankAccountList: BankAccountList | undefined, policyCurrency: string | undefined) { + if (!bankAccountList || policyCurrency === undefined) { + return []; + } + + return Object.values(bankAccountList).filter((account) => { + return account.bankCurrency === policyCurrency && account.accountData?.state === CONST.BANK_ACCOUNT.STATE.OPEN && account.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS; + }); +} + +export { + calculateApprovers, + convertPolicyEmployeesToApprovalWorkflows, + convertApprovalWorkflowToPolicyEmployees, + getEligibleExistingBusinessBankAccounts, + INITIAL_APPROVAL_WORKFLOW, + updateWorkflowDataOnApproverRemoval, +}; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index f3214aa90ea7..f1e817d0035c 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -1,5 +1,5 @@ import {createRef} from 'react'; -import type {MutableRefObject} from 'react'; +import type {RefObject} from 'react'; import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -37,7 +37,7 @@ type KYCWallRef = { /** * Sets up a ref to an instance of the KYC Wall component. */ -const kycWallRef: MutableRefObject = createRef(); +const kycWallRef: RefObject = createRef(); /** * When we successfully add a payment method or pass the KYC checks we will continue with our setup action if we have one set. @@ -53,7 +53,7 @@ function continueSetup(fallbackRoute?: Route) { kycWallRef.current.continueAction(); } -function openWalletPage() { +function getPaymentMethods() { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -582,7 +582,7 @@ function setInvoicingTransferBankAccount(bankAccountID: number, policyID: string export { deletePaymentCard, addPaymentCard, - openWalletPage, + getPaymentMethods, makeDefaultPaymentMethod, kycWallRef, continueSetup, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 205d64bf5ad8..4788c8c777be 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -182,6 +182,13 @@ type DuplicatePolicyDataOptions = { policyCategories?: PolicyCategories; }; +type SetWorkspaceReimbursementActionParams = { + policyID: string; + reimbursementChoice: ValueOf; + bankAccountID?: number; + reimburserEmail: string; +}; + const allPolicies: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, @@ -866,7 +873,7 @@ function clearQuickbooksOnlineAutoSyncErrorField(policyID: string | undefined) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {quickbooksOnline: {config: {errorFields: {autoSync: null}}}}}); } -function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserEmail: string) { +function setWorkspaceReimbursement({policyID, reimbursementChoice, bankAccountID, reimburserEmail}: SetWorkspaceReimbursementActionParams) { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation const policy = getPolicy(policyID); @@ -879,7 +886,7 @@ function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueO reimbursementChoice, isLoadingWorkspaceReimbursement: true, reimburser: reimburserEmail, - achAccount: {reimburser: reimburserEmail}, + achAccount: {reimburser: reimburserEmail, bankAccountID}, errorFields: {reimbursementChoice: null}, pendingFields: {reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }, @@ -905,14 +912,14 @@ function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueO value: { isLoadingWorkspaceReimbursement: false, reimbursementChoice: policy?.reimbursementChoice ?? null, - achAccount: {reimburser: policy?.achAccount?.reimburser ?? null}, + achAccount: {reimburser: policy?.achAccount?.reimburser ?? null, bankAccountID: null}, errorFields: {reimbursementChoice: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, pendingFields: {reimbursementChoice: null}, }, }, ]; - const params: SetWorkspaceReimbursementParams = {policyID, reimbursementChoice}; + const params: SetWorkspaceReimbursementParams = {policyID, reimbursementChoice, bankAccountID}; API.write(WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT, params, {optimisticData, failureData, successData}); } diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index a03cba87f9b3..8d3f2c503787 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -25,7 +25,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} from '@libs/ReimbursementAccountUtils'; +import {getRouteForCurrentStep, hasInProgressUSDVBBA, hasInProgressVBBA} from '@libs/ReimbursementAccountUtils'; import shouldReopenOnfido from '@libs/shouldReopenOnfido'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; @@ -126,26 +126,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: }; } - /** - * Returns true if a VBBA exists in any state other than OPEN or LOCKED - */ - const hasInProgressVBBA = useCallback((): boolean => { - return !!achData?.bankAccountID && !!achData?.state && achData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN && achData?.state !== CONST.BANK_ACCOUNT.STATE.LOCKED; - }, [achData?.bankAccountID, achData?.state]); - - /** Returns true if user passed first step of flow for non USD VBBA */ - const hasInProgressNonUSDVBBA = useCallback((): boolean => { - return (!!achData?.bankAccountID && !!achData?.created) || nonUSDCountryDraftValue !== ''; - }, [achData?.bankAccountID, achData?.created, nonUSDCountryDraftValue]); - - /** Returns true if VBBA flow is in progress */ const shouldShowContinueSetupButtonValue = useMemo(() => { - if (isNonUSDWorkspace) { - return hasInProgressNonUSDVBBA(); - } - - return hasInProgressVBBA(); - }, [isNonUSDWorkspace, hasInProgressNonUSDVBBA, hasInProgressVBBA]); + return hasInProgressVBBA(achData, isNonUSDWorkspace, nonUSDCountryDraftValue); + }, [achData, isNonUSDWorkspace, nonUSDCountryDraftValue]); /** When this page is first opened, `reimbursementAccount` prop might not yet be fully loaded from Onyx. @@ -229,7 +212,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: prevReimbursementAccount.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && reimbursementAccount?.pendingAction !== prevReimbursementAccount.pendingAction ) { - setShouldShowContinueSetupButton(hasInProgressVBBA()); + setShouldShowContinueSetupButton(hasInProgressUSDVBBA(achData)); } if (shouldShowContinueSetupButton) { @@ -330,7 +313,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: switch (currentStep) { case CONST.BANK_ACCOUNT.STEP.COUNTRY: - if (hasInProgressVBBA()) { + if (hasInProgressUSDVBBA(achData)) { setShouldShowContinueSetupButton(true); } setUSDBankAccountStep(null); @@ -375,7 +358,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: default: Navigation.dismissModal(); } - }, [achData?.isOnfidoSetupComplete, achData?.state, currentStep, hasInProgressVBBA, isOffline, onfidoToken]); + }, [achData, currentStep, isOffline, onfidoToken]); const isLoading = (isLoadingApp || (reimbursementAccount?.isLoading && !reimbursementAccount?.isCreateCorpayBankAccount)) && diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index b2c609f4a791..908bdaa1a644 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -1,14 +1,11 @@ import {FlashList} from '@shopify/flash-list'; import lodashSortBy from 'lodash/sortBy'; -import type {ReactElement, Ref} from 'react'; +import type {ReactElement} from 'react'; import React, {useCallback, useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {SvgProps} from 'react-native-svg/lib/typescript/ReactNativeSVG'; import type {ValueOf} from 'type-fest'; import type {RenderSuggestionMenuItemProps} from '@components/AutoCompleteSuggestions/types'; -import Button from '@components/Button'; -import FormAlertWrapper from '@components/FormAlertWrapper'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -16,7 +13,6 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import type {FormattedSelectedPaymentMethodIcon} from '@hooks/usePaymentMethodState/types'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -30,25 +26,18 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {AccountData, BankAccount, BankAccountList, Card, CardList, CompanyCardFeed} from '@src/types/onyx'; +import type {BankAccount, BankAccountList, CardList, CompanyCardFeed} from '@src/types/onyx'; import type {BankIcon} from '@src/types/onyx/Bank'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type PaymentMethod from '@src/types/onyx/PaymentMethod'; -import type {FilterMethodPaymentType} from '@src/types/onyx/WalletTransfer'; import {getEmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import type {CardPressHandlerParams, PaymentMethodPressHandlerParams} from './WalletPage/types'; -type PaymentMethodPressHandler = ( - event?: GestureResponderEvent | KeyboardEvent, - accountType?: string, - accountData?: AccountData, - icon?: FormattedSelectedPaymentMethodIcon, - isDefault?: boolean, - methodID?: number, - description?: string, -) => void; +type PaymentMethodPressHandler = ({event, accountType, accountData, methodID, icon, description, isDefault}: PaymentMethodPressHandlerParams) => void; -type CardPressHandler = (event?: GestureResponderEvent | KeyboardEvent, cardData?: Card, icon?: FormattedSelectedPaymentMethodIcon, cardID?: number) => void; +type CardPressHandler = ({event, cardID, cardData, icon}: CardPressHandlerParams) => void; type PaymentMethodListProps = { /** Type of active/highlighted payment method */ @@ -69,33 +58,18 @@ type PaymentMethodListProps = { /** Should menu items be selectable with a checkbox */ shouldShowSelectedState?: boolean; - /** React ref being forwarded to the PaymentMethodList Button */ - buttonRef?: Ref; - /** List container style */ style?: StyleProp; /** List item style */ listItemStyle?: StyleProp; - /** Type to filter the payment Method list */ - filterType?: FilterMethodPaymentType; - /** Whether the add bank account button should be shown on the list */ shouldShowAddBankAccount?: boolean; - /** Whether the add Payment button be shown on the list */ - shouldShowAddPaymentMethodButton?: boolean; - - /** Whether the add Bank account button be shown on the list */ - shouldShowAddBankAccountButton?: boolean; - /** Whether the assigned cards should be shown on the list */ shouldShowAssignedCards?: boolean; - /** Whether the empty list message should be shown when the list is empty */ - shouldShowEmptyListMessage?: boolean; - /** Whether the right icon should be shown in PaymentMethodItem */ shouldShowRightIcon?: boolean; @@ -107,6 +81,18 @@ type PaymentMethodListProps = { /** Whether the bank accounts should be displayed in private and business sections */ shouldShowBankAccountSections?: boolean; + + /** Function to be called when the user presses the add bank account button */ + onAddBankAccountPress?: () => void; + + /** The icon to be displayed in the right side of the payment method item */ + itemIconRight?: IconAsset; + + /** Type of payment method to filter by */ + filterType?: ValueOf; + + /** Whether to show the default badge for the payment method */ + shouldHideDefaultBadge?: boolean; }; type PaymentMethodItem = PaymentMethod & { @@ -121,7 +107,7 @@ type PaymentMethodItem = PaymentMethod & { interactive?: boolean; brickRoadIndicator?: ValueOf; errors?: Errors; - iconRight?: React.FC; + iconRight?: IconAsset; isMethodActive?: boolean; cardID?: number; plaidUrl?: string; @@ -155,8 +141,8 @@ function dismissError(item: PaymentMethodItem) { } } -function shouldShowDefaultBadge(filteredPaymentMethods: PaymentMethod[], isDefault = false): boolean { - if (!isDefault) { +function shouldShowDefaultBadge(filteredPaymentMethods: PaymentMethod[], isDefault = false, shouldHideDefaultBadge = false): boolean { + if (!isDefault || shouldHideDefaultBadge) { return false; } const defaultPaymentMethodCount = filteredPaymentMethods.filter( @@ -179,15 +165,10 @@ function keyExtractor(item: PaymentMethod | string) { function PaymentMethodList({ actionPaymentMethodType = '', activePaymentMethodID = '', - buttonRef = () => {}, - filterType = '', listHeaderComponent, onPress, shouldShowSelectedState = false, - shouldShowAddPaymentMethodButton = true, - shouldShowAddBankAccountButton = false, shouldShowAddBankAccount = true, - shouldShowEmptyListMessage = true, shouldShowAssignedCards = false, selectedMethodID = '', onListContentSizeChange = () => {}, @@ -196,6 +177,10 @@ function PaymentMethodList({ shouldShowRightIcon = true, invoiceTransferBankAccountID, shouldShowBankAccountSections = false, + onAddBankAccountPress = () => {}, + itemIconRight, + filterType, + shouldHideDefaultBadge = false, }: PaymentMethodListProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -203,7 +188,10 @@ function PaymentMethodList({ const {isOffline} = useNetwork(); const illustrations = useThemeIllustrations(); - const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.validated, canBeMissing: true}); + const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, { + selector: (account) => account?.validated, + canBeMissing: true, + }); const [bankAccountList = getEmptyObject(), bankAccountListResult] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true}); const isLoadingBankAccountList = isLoadingOnyxValue(bankAccountListResult); @@ -211,8 +199,6 @@ function PaymentMethodList({ const isLoadingCardList = isLoadingOnyxValue(cardListResult); // Temporarily disabled because P2P debit cards are disabled. // const [fundList = getEmptyObject()] = useOnyx(ONYXKEYS.FUND_LIST); - const [isLoadingPaymentMethods = true, isLoadingPaymentMethodsResult] = useOnyx(ONYXKEYS.IS_LOADING_PAYMENT_METHODS, {canBeMissing: true}); - const isLoadingPaymentMethodsOnyx = isLoadingOnyxValue(isLoadingPaymentMethodsResult); const filteredPaymentMethods = useMemo(() => { if (shouldShowAssignedCards) { @@ -252,20 +238,20 @@ function PaymentMethodList({ iconStyles: [styles.cardIcon], iconWidth: variables.cardIconWidth, iconHeight: variables.cardIconHeight, - iconRight: Expensicons.ThreeDots, + iconRight: itemIconRight ?? Expensicons.ThreeDots, isMethodActive: activePaymentMethodID === card.cardID, onPress: (e: GestureResponderEvent | KeyboardEvent | undefined) => - pressHandler( - e, - card, - { + pressHandler({ + event: e, + cardData: card, + icon: { icon, iconStyles: [styles.cardIcon], iconWidth: variables.cardIconWidth, iconHeight: variables.cardIconHeight, }, - card.cardID, - ), + cardID: card.cardID, + }), }); return; } @@ -325,97 +311,83 @@ function PaymentMethodList({ const filteredCardList = {}; let combinedPaymentMethods = formatPaymentMethods(isLoadingBankAccountList ? {} : (bankAccountList ?? {}), filteredCardList, styles); - if (filterType !== '') { - combinedPaymentMethods = combinedPaymentMethods.filter((paymentMethod) => paymentMethod.accountType === filterType); - } - if (!isOffline) { combinedPaymentMethods = combinedPaymentMethods.filter( (paymentMethod) => paymentMethod.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(paymentMethod.errors), ); } + + if (filterType) { + combinedPaymentMethods = combinedPaymentMethods.filter((paymentMethod) => (paymentMethod as BankAccount).accountData?.type === filterType); + } + combinedPaymentMethods = combinedPaymentMethods.map((paymentMethod) => { const pressHandler = onPress as PaymentMethodPressHandler; const isMethodActive = isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod); return { ...paymentMethod, onPress: (e: GestureResponderEvent) => - pressHandler( - e, - paymentMethod.accountType, - paymentMethod.accountData, - { + pressHandler({ + event: e, + accountType: paymentMethod.accountType, + accountData: paymentMethod.accountData, + icon: { icon: paymentMethod.icon, iconHeight: paymentMethod?.iconHeight, iconWidth: paymentMethod?.iconWidth, iconStyles: paymentMethod?.iconStyles, iconSize: paymentMethod?.iconSize, }, - paymentMethod.isDefault, - paymentMethod.methodID, - paymentMethod.description, - ), + isDefault: paymentMethod.isDefault, + methodID: paymentMethod.methodID, + description: paymentMethod.description, + }), wrapperStyle: isMethodActive ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : null, disabled: paymentMethod.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, isMethodActive, - iconRight: Expensicons.ThreeDots, + iconRight: itemIconRight ?? Expensicons.ThreeDots, shouldShowRightIcon, }; }); return combinedPaymentMethods; }, [ shouldShowAssignedCards, + isLoadingBankAccountList, bankAccountList, styles, - filterType, isOffline, - cardList, - actionPaymentMethodType, - activePaymentMethodID, - StyleUtils, - shouldShowRightIcon, - onPress, - isLoadingBankAccountList, + filterType, isLoadingCardList, + cardList, illustrations, translate, + onPress, + shouldShowRightIcon, + itemIconRight, + activePaymentMethodID, + actionPaymentMethodType, + StyleUtils, ]); - /** - * Render placeholder when there are no payments methods - */ - const renderListEmptyComponent = () => {translate('paymentMethodList.addFirstPaymentMethod')}; - const onPressItem = useCallback(() => { if (!isUserValidated) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute(Navigation.getActiveRoute(), ROUTES.SETTINGS_ADD_BANK_ACCOUNT.route)); return; } - onPress(); - }, [isUserValidated, onPress]); + onAddBankAccountPress(); + }, [isUserValidated, onAddBankAccountPress]); const renderListFooterComponent = useCallback( - () => - shouldShowAddBankAccountButton ? ( -