diff --git a/src/CONST.ts b/src/CONST.ts index e7358b382f14..240025e7464f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1260,6 +1260,24 @@ const CONST = { TERMS: 'TermsStep', ACTIVATE: 'ActivateStep', }, + STEP_REFACTOR: { + ADD_BANK_ACCOUNT: 'AddBankAccountStep', + ADDITIONAL_DETAILS: 'AdditionalDetailsStep', + VERIFY_IDENTITY: 'VerifyIdentityStep', + TERMS_AND_FEES: 'TermsAndFeesStep', + }, + STEP_NAMES: ['1', '2', '3', '4'], + SUBSTEP_INDEXES: { + BANK_ACCOUNT: { + ACCOUNT_NUMBERS: 0, + }, + PERSONAL_INFO: { + LEGAL_NAME: 0, + DATE_OF_BIRTH: 1, + SSN: 2, + ADDRESS: 3, + }, + }, TIER_NAME: { PLATINUM: 'PLATINUM', GOLD: 'GOLD', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 46b2c5f8055c..0046e076e056 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -430,8 +430,8 @@ const ONYXKEYS = { REPORT_FIELD_EDIT_FORM_DRAFT: 'reportFieldEditFormDraft', REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount', REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', - PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm', - PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', + PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccountForm', + PERSONAL_BANK_ACCOUNT_FORM_DRAFT: 'personalBankAccountFormDraft', EXIT_SURVEY_REASON_FORM: 'exitSurveyReasonForm', EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft', EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm', @@ -491,7 +491,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: FormTypes.GetPhysicalCardForm; [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: FormTypes.ReportFieldEditForm; [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; - [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm; + [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; [ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS]: FormTypes.AdditionalDetailStepForm; [ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 8130c271a2db..4034db2ee0c1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -109,6 +109,7 @@ const ROUTES = { }, SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', + SETTINGS_ADD_BANK_ACCOUNT_REFACTOR: 'settings/wallet/add-bank-account-refactor', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: { route: 'settings/wallet/card/:domain/digital-details/update-address', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index cf864fd96b3e..c1d6a5669fbc 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -32,6 +32,7 @@ const SCREENS = { APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links', ADD_DEBIT_CARD: 'Settings_Add_Debit_Card', ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', + ADD_BANK_ACCOUNT_REFACTOR: 'Settings_Add_Bank_Account_Refactor', CLOSE: 'Settings_Close', TWO_FACTOR_AUTH: 'Settings_TwoFactorAuth', REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged', diff --git a/src/components/AddPlaidBankAccount.tsx b/src/components/AddPlaidBankAccount.tsx index 4111d9cc8e6f..366f14ec9780 100644 --- a/src/components/AddPlaidBankAccount.tsx +++ b/src/components/AddPlaidBankAccount.tsx @@ -62,6 +62,9 @@ type AddPlaidBankAccountProps = AddPlaidBankAccountOnyxProps & { /** Is displayed in new VBBA */ isDisplayedInNewVBBA?: boolean; + /** Is displayed in new enable wallet flow */ + isDisplayedInWalletFlow?: boolean; + /** Text to display on error message */ errorText?: string; @@ -84,6 +87,7 @@ function AddPlaidBankAccount({ isDisplayedInNewVBBA = false, errorText = '', onInputChange = () => {}, + isDisplayedInWalletFlow = false, }: AddPlaidBankAccountProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -255,10 +259,10 @@ function AddPlaidBankAccount({ return {renderPlaidLink()}; } - if (isDisplayedInNewVBBA) { + if (isDisplayedInNewVBBA || isDisplayedInWalletFlow) { return ( - {translate('bankAccount.chooseAnAccount')} + {translate(isDisplayedInWalletFlow ? 'walletPage.chooseYourBankAccount' : 'bankAccount.chooseAnAccount')} {!!text && {text}} require('../../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType, [SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default as React.ComponentType, + [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_REFACTOR]: () => require('../../../../pages/EnablePayments/AddBankAccount/AddBankAccount').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.STATUS]: () => require('../../../../pages/settings/Profile/CustomStatus/StatusPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER]: () => require('../../../../pages/settings/Profile/CustomStatus/StatusClearAfterPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE]: () => require('../../../../pages/settings/Profile/CustomStatus/SetDatePage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 823b6514c42b..c777a689b624 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -156,6 +156,10 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_ADD_BANK_ACCOUNT, exact: true, }, + [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_REFACTOR]: { + path: ROUTES.SETTINGS_ADD_BANK_ACCOUNT_REFACTOR, + exact: true, + }, [SCREENS.SETTINGS.PROFILE.PRONOUNS]: { path: ROUTES.SETTINGS_PRONOUNS, exact: true, diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index bb20b2b686ab..ad8f9eea6ecb 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import type {OnfidoDataWithApplicantID} from '@components/Onfido/types'; import * as API from '@libs/API'; import type { @@ -18,6 +19,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; +import type {PersonalBankAccountForm} from '@src/types/form'; import type {ACHContractStepProps, BeneficialOwnersStepProps, CompanyStepProps, RequestorStepProps} from '@src/types/form/ReimbursementAccountForm'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; import type {BankAccountStep, BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; @@ -42,6 +44,8 @@ type ReimbursementAccountStep = BankAccountStep | ''; type ReimbursementAccountSubStep = BankAccountSubStep | ''; +type AccountFormValues = typeof ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM | typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM; + type BusinessAddress = { addressStreet?: string; addressCity?: string; @@ -82,6 +86,23 @@ function openPersonalBankAccountSetupView(exitReportID?: string) { }); } +/** + * TODO: remove the previous function and rename this function to openPersonalBankAccountSetupView after migrating to the new flow + * Open the personal bank account setup flow, with an optional exitReportID to redirect to once the flow is finished. + */ +function openPersonalBankAccountSetupViewRefactor(exitReportID?: string) { + clearPlaid().then(() => { + if (exitReportID) { + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {exitReportID}); + } + Onyx.merge(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {setupType: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}); + }); +} + +function clearPersonalBankAccountSetupType() { + Onyx.merge(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {setupType: null}); +} + /** * Whether after adding a bank account we should continue with the KYC flow. If so, we must specify the fallback route. */ @@ -92,6 +113,8 @@ function setPersonalBankAccountContinueKYCOnSuccess(onSuccessFallbackRoute: Rout function clearPersonalBankAccount() { clearPlaid(); Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {}); + Onyx.set(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, null); + clearPersonalBankAccountSetupType(); } function clearOnfidoToken() { @@ -99,6 +122,10 @@ function clearOnfidoToken() { Onyx.merge(ONYXKEYS.ONFIDO_APPLICANT_ID, ''); } +function updateAddPersonalBankAccountDraft(bankData: Partial) { + Onyx.merge(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, bankData); +} + /** * Helper method to build the Onyx data required during setup of a Verified Business Bank Account */ @@ -503,6 +530,16 @@ function setReimbursementAccountLoading(isLoading: boolean) { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading}); } +function validatePlaidSelection(values: FormOnyxValues): FormInputErrors { + const errorFields: FormInputErrors = {}; + + if (!values.selectedPlaidAccountID) { + errorFields.selectedPlaidAccountID = 'bankAccount.error.youNeedToSelectAnOption'; + } + + return errorFields; +} + export { acceptACHContractForBankAccount, addBusinessWebsiteForDraft, @@ -527,6 +564,10 @@ export { validateBankAccount, verifyIdentityForBankAccount, setReimbursementAccountLoading, + openPersonalBankAccountSetupViewRefactor, + updateAddPersonalBankAccountDraft, + clearPersonalBankAccountSetupType, + validatePlaidSelection, }; export type {BusinessAddress, PersonalAddress}; diff --git a/src/libs/getPlaidDesktopMessage/types.ts b/src/libs/getPlaidDesktopMessage/types.ts index 12a13737339c..95a6254e997b 100644 --- a/src/libs/getPlaidDesktopMessage/types.ts +++ b/src/libs/getPlaidDesktopMessage/types.ts @@ -1,3 +1,5 @@ -type GetPlaidDesktopMessage = () => string | undefined; +import type {TranslationPaths} from '@src/languages/types'; + +type GetPlaidDesktopMessage = () => TranslationPaths | undefined; export default GetPlaidDesktopMessage; diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index e2d832f672b2..e9c093133d55 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -80,7 +80,7 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona /> ) : ( ; + + /** The details about the Personal bank account we are adding saved in Onyx */ + personalBankAccount: OnyxEntry; + + /** The draft values of the bank account being setup */ + personalBankAccountDraft: OnyxEntry; +}; + +const plaidSubsteps: Array> = [Plaid, Confirmation]; + +function AddBankAccount({personalBankAccount, plaidData, personalBankAccountDraft}: AddPersonalBankAccountPageWithOnyxProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const submit = useCallback(() => { + const bankAccounts = plaidData?.bankAccounts ?? []; + const selectedPlaidBankAccount = bankAccounts.find((bankAccount) => bankAccount.plaidAccountID === personalBankAccountDraft?.plaidAccountID); + + if (selectedPlaidBankAccount) { + BankAccounts.addPersonalBankAccount(selectedPlaidBankAccount); + Navigation.navigate(ROUTES.SETTINGS_ENABLE_PAYMENTS); + } + }, [personalBankAccountDraft?.plaidAccountID, plaidData?.bankAccounts]); + + const isSetupTypeChosen = personalBankAccountDraft?.setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID; + + const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo} = useSubStep({bodyContent: plaidSubsteps, startFrom: 0, onFinished: submit}); + + const exitFlow = (shouldContinue = false) => { + const exitReportID = personalBankAccount?.exitReportID; + const onSuccessFallbackRoute = personalBankAccount?.onSuccessFallbackRoute ?? ''; + + if (exitReportID) { + Navigation.dismissModal(exitReportID); + return; + } + if (shouldContinue && onSuccessFallbackRoute) { + PaymentMethods.continueSetup(onSuccessFallbackRoute); + return; + } + Navigation.goBack(ROUTES.SETTINGS_WALLET); + }; + + const handleBackButtonPress = () => { + if (!isSetupTypeChosen) { + exitFlow(); + return; + } + if (screenIndex === 0) { + BankAccounts.clearPersonalBankAccount(); + return; + } + prevScreen(); + }; + + useEffect(() => BankAccounts.clearPersonalBankAccount, []); + + return ( + + + {isSetupTypeChosen ? ( + <> + + + + + + ) : ( + + )} + + ); +} + +AddBankAccount.displayName = 'AddBankAccountPage'; + +export default withOnyx({ + plaidData: { + key: ONYXKEYS.PLAID_DATA, + }, + personalBankAccount: { + key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, + }, + personalBankAccountDraft: { + key: ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, + }, +})(AddBankAccount); diff --git a/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx b/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx new file mode 100644 index 000000000000..c03e983d5c4f --- /dev/null +++ b/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import Button from '@components/Button'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import getPlaidDesktopMessage from '@libs/getPlaidDesktopMessage'; +import * as BankAccounts from '@userActions/BankAccounts'; +import CONFIG from '@src/CONFIG'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {User} from '@src/types/onyx'; + +type SetupMethodOnyxProps = { + /** The user's data */ + user: OnyxEntry; + + /** Whether Plaid is disabled */ + isPlaidDisabled: OnyxEntry; +}; + +type SetupMethodProps = SetupMethodOnyxProps; + +const plaidDesktopMessage = getPlaidDesktopMessage(); +const bankAccountRoute = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.SETTINGS_ADD_BANK_ACCOUNT_REFACTOR}`; + +function SetupMethod({isPlaidDisabled, user}: SetupMethodProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( +
+ + {translate('walletPage.addBankAccountBody')} + + {!!plaidDesktopMessage && ( + + {translate(plaidDesktopMessage)} + + )} +
+ ); +} + +SetupMethod.displayName = 'SetupMethod'; + +export default withOnyx({ + isPlaidDisabled: { + key: ONYXKEYS.IS_PLAID_DISABLED, + }, + user: { + key: ONYXKEYS.USER, + }, +})(SetupMethod); diff --git a/src/pages/EnablePayments/AddBankAccount/substeps/ConfirmationStep.tsx b/src/pages/EnablePayments/AddBankAccount/substeps/ConfirmationStep.tsx new file mode 100644 index 000000000000..f274c0646c1a --- /dev/null +++ b/src/pages/EnablePayments/AddBankAccount/substeps/ConfirmationStep.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import DotIndicatorMessage from '@components/DotIndicatorMessage'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalBankAccountForm} from '@src/types/form'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {PlaidData, UserWallet} from '@src/types/onyx'; + +type ConfirmationOnyxProps = { + /** The draft values of the bank account being setup */ + personalBankAccountDraft: OnyxEntry; + + /** Contains plaid data */ + plaidData: OnyxEntry; + + /** The user's wallet */ + userWallet: OnyxEntry; +}; + +type ConfirmationStepProps = ConfirmationOnyxProps & SubStepProps; + +const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; +const BANK_INFO_STEP_INDEXES = CONST.WALLET.SUBSTEP_INDEXES.BANK_ACCOUNT; + +function ConfirmationStep({personalBankAccountDraft, plaidData, onNext, onMove, userWallet}: ConfirmationStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + const isLoading = plaidData?.isLoading ?? false; + const error = ErrorUtils.getLatestErrorMessage(userWallet ?? {}); + + const handleModifyAccountNumbers = () => { + onMove(BANK_INFO_STEP_INDEXES.ACCOUNT_NUMBERS); + }; + + return ( + + {translate('walletPage.confirmYourBankAccount')} + {translate('bankAccount.letsDoubleCheck')} + + + {error && error.length > 0 && ( + + )} +