From a224f664118e8c5abade6966cfae1fd5908a3bb2 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Wed, 21 Sep 2022 11:26:12 +0200 Subject: [PATCH 1/7] Refactor VBBA setup flow with new API ConnectBankAccountWithPlaid --- src/components/AddPlaidBankAccount.js | 44 +- src/libs/ReimbursementAccountUtils.js | 4 +- src/libs/ValidationUtils.js | 4 +- src/libs/actions/BankAccounts.js | 66 ++- src/libs/actions/Plaid.js | 6 +- .../actions/ReimbursementAccount/errors.js | 29 +- .../fetchFreePlanVerifiedBankAccount.js | 9 +- .../actions/ReimbursementAccount/index.js | 1 - .../setupWithdrawalAccount.js | 21 +- src/pages/AddPersonalBankAccountPage.js | 8 +- .../BankAccountManualStep.js | 171 ++++++++ .../BankAccountPlaidStep.js | 102 +++++ .../ReimbursementAccount/BankAccountStep.js | 401 +++++------------- src/pages/ReimbursementAccount/CompanyStep.js | 116 +++-- .../ReimbursementAccountDraftPropTypes.js | 56 +++ .../ReimbursementAccountForm.js | 50 ++- .../ReimbursementAccountPage.js | 6 +- .../ReimbursementAccount/RequestorStep.js | 4 +- .../ReimbursementAccount/ValidationStep.js | 19 +- .../plaidDataPropTypes.js | 2 +- .../reimbursementAccountPropTypes.js | 4 +- .../Payments/PaymentsPage/BasePaymentsPage.js | 4 +- .../workspace/WorkspaceBankAccountPage.js | 2 +- 23 files changed, 639 insertions(+), 490 deletions(-) create mode 100644 src/pages/ReimbursementAccount/BankAccountManualStep.js create mode 100644 src/pages/ReimbursementAccount/BankAccountPlaidStep.js create mode 100644 src/pages/ReimbursementAccount/ReimbursementAccountDraftPropTypes.js diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index c0f926430b0c..926a47118177 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -58,7 +58,7 @@ const defaultProps = { bankName: '', plaidAccessToken: '', bankAccounts: [], - loading: false, + isLoading: false, error: '', }, plaidLinkToken: '', @@ -77,11 +77,6 @@ class AddPlaidBankAccount extends React.Component { this.selectAccount = this.selectAccount.bind(this); this.getPlaidLinkToken = this.getPlaidLinkToken.bind(this); - - this.state = { - selectedIndex: undefined, - institution: {}, - }; } componentDidMount() { @@ -91,8 +86,9 @@ class AddPlaidBankAccount extends React.Component { return; } - BankAccounts.clearPlaid(); - BankAccounts.openPlaidBankLogin(this.props.allowDebit, this.props.bankAccountID); + if (_.isEmpty(this.props.plaidData)) { + BankAccounts.openPlaidBankLogin(this.props.allowDebit, this.props.bankAccountID); + } } /** @@ -119,30 +115,30 @@ class AddPlaidBankAccount extends React.Component { /** * Triggered when user selects a Plaid bank account. - * @param {String} index + * @param {String} plaidAccountID */ - selectAccount(index) { - this.setState({selectedIndex: Number(index)}, () => { - const selectedPlaidBankAccount = this.getPlaidBankAccounts()[this.state.selectedIndex]; - selectedPlaidBankAccount.bankName = this.props.plaidData.bankName; - selectedPlaidBankAccount.plaidAccessToken = this.props.plaidData.plaidAccessToken; - this.props.onSelect({selectedPlaidBankAccount}); - }); + selectAccount(plaidAccountID) { + const selectedPlaidBankAccount = _.findWhere(this.getPlaidBankAccounts(), {plaidAccountID}); + selectedPlaidBankAccount.bankName = this.props.plaidData.bankName; + selectedPlaidBankAccount.plaidAccessToken = this.props.plaidData.plaidAccessToken; + this.props.onSelect({selectedPlaidBankAccount}); } render() { const plaidBankAccounts = this.getPlaidBankAccounts(); const token = this.getPlaidLinkToken(); - const options = _.map(plaidBankAccounts, (account, index) => ({ - value: index, label: `${account.addressName} ${account.mask}`, + const options = _.map(plaidBankAccounts, account => ({ + value: account.plaidAccountID, label: `${account.addressName} ${account.mask}`, })); - const {icon, iconSize} = getBankIcon(this.state.institution.name); + const institutionName = lodashGet(this.props, 'plaidData.institution.name', ''); + const selectedPlaidBankAacount = lodashGet(this.props, 'plaidData.selectedPlaidBankAccount', {}); + const {icon, iconSize} = getBankIcon(); // Plaid Link view if (!plaidBankAccounts.length) { return ( - {(!token || this.props.plaidData.loading) + {(!token || this.props.plaidData.isLoading) && ( @@ -159,7 +155,7 @@ class AddPlaidBankAccount extends React.Component { onSuccess={({publicToken, metadata}) => { Log.info('[PlaidLink] Success!'); BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, this.props.allowDebit); - this.setState({institution: metadata.institution}); + BankAccounts.updatePlaidData({institution: metadata.institution}); }} onError={(error) => { Log.hmmm('[PlaidLink] Error: ', error.message); @@ -187,18 +183,18 @@ class AddPlaidBankAccount extends React.Component { height={iconSize} width={iconSize} /> - {this.state.institution.name} + {institutionName} diff --git a/src/libs/ReimbursementAccountUtils.js b/src/libs/ReimbursementAccountUtils.js index 67bdae4b6f92..5f7ce0782367 100644 --- a/src/libs/ReimbursementAccountUtils.js +++ b/src/libs/ReimbursementAccountUtils.js @@ -3,7 +3,7 @@ import * as BankAccounts from './actions/BankAccounts'; import FormHelper from './FormHelper'; const formHelper = new FormHelper({ - errorPath: 'reimbursementAccount.errors', + errorPath: 'reimbursementAccount.errorFields', setErrors: BankAccounts.setBankAccountFormValidationErrors, }); @@ -32,7 +32,7 @@ function getDefaultStateForField(props, fieldName, defaultValue = '') { * @returns {String} */ function getErrorText(props, errorTranslationKeys, inputKey) { - const errors = getErrors(props); + const errors = getErrors(props) || {}; return errors[inputKey] ? props.translate(errorTranslationKeys[inputKey]) : ''; } diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index 02b64dda758d..1573f9f74725 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -258,7 +258,7 @@ function validateIdentity(identity) { */ function isValidUSPhone(phoneNumber = '', isCountryCodeOptional) { // Remove non alphanumeric characters from the phone number - const sanitizedPhone = phoneNumber.replace(CONST.REGEX.NON_ALPHA_NUMERIC, ''); + const sanitizedPhone = (phoneNumber || '').replace(CONST.REGEX.NON_ALPHA_NUMERIC, ''); const isUsPhone = isCountryCodeOptional ? CONST.REGEX.US_PHONE_WITH_OPTIONAL_COUNTRY_CODE.test(sanitizedPhone) : CONST.REGEX.US_PHONE.test(sanitizedPhone); @@ -370,7 +370,7 @@ function isExistingRoomName(roomName, reports, policyID) { * @returns {Boolean} */ function isValidTaxID(taxID) { - return CONST.REGEX.TAX_ID.test(taxID.replace(CONST.REGEX.NON_NUMERIC, '')); + return taxID && CONST.REGEX.TAX_ID.test(taxID.replace(CONST.REGEX.NON_NUMERIC, '')); } export { diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 021592b4babe..2959b90dfc20 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -9,8 +9,6 @@ export { setupWithdrawalAccount, fetchFreePlanVerifiedBankAccount, goToWithdrawalAccountSetupStep, - showBankAccountErrorModal, - showBankAccountFormValidationError, setBankAccountFormValidationErrors, resetReimbursementAccount, resetFreePlanBankAccount, @@ -42,6 +40,62 @@ function clearPlaid() { Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, ''); } +function updatePlaidData(plaidData) { + Onyx.merge(ONYXKEYS.PLAID_DATA, plaidData); +} + +function getOnyxDataForVBBA() { + return { + optimisticData: [ + { + onyxMethod: 'merge', + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: true, + errors: null, + }, + }, + ], + successData: [ + { + onyxMethod: 'merge', + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, + errors: null, + }, + }, + ], + failureData: [ + { + onyxMethod: 'merge', + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('paymentsPage.addBankAccountFailure'), + }, + }, + }, + ], + }; +} + +function connectBankAccountWithPlaid(bankAccountID, selectedPlaidBankAccount) { + const commandName = 'ConnectBankAccountWithPlaid'; + + const parameters = { + bankAccountID, + routingNumber: selectedPlaidBankAccount.routingNumber, + accountNumber: selectedPlaidBankAccount.accountNumber, + bank: selectedPlaidBankAccount.bankName, + plaidAccountID: selectedPlaidBankAccount.plaidAccountID, + plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken, + }; + + API.write(commandName, parameters, getOnyxDataForVBBA()); +} + /** * Helper method to build the Onyx data required during setup of a Verified Business Bank Account * @@ -114,7 +168,7 @@ function addPersonalBankAccount(account, password) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, value: { - loading: true, + isLoading: true, error: '', }, }, @@ -124,7 +178,7 @@ function addPersonalBankAccount(account, password) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, value: { - loading: false, + isLoading: false, error: '', shouldShowSuccess: true, }, @@ -135,7 +189,7 @@ function addPersonalBankAccount(account, password) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, value: { - loading: false, + isLoading: false, error: Localize.translateLocal('paymentsPage.addBankAccountFailure'), }, }, @@ -199,4 +253,6 @@ export { clearPersonalBankAccount, clearPlaid, validateBankAccount, + connectBankAccountWithPlaid, + updatePlaidData, }; diff --git a/src/libs/actions/Plaid.js b/src/libs/actions/Plaid.js index dc88c99c2647..f6691b8f4a3c 100644 --- a/src/libs/actions/Plaid.js +++ b/src/libs/actions/Plaid.js @@ -31,7 +31,7 @@ function openPlaidBankAccountSelector(publicToken, bankName, allowDebit) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.PLAID_DATA, value: { - loading: true, + isLoading: true, error: '', bankName, }, @@ -40,7 +40,7 @@ function openPlaidBankAccountSelector(publicToken, bankName, allowDebit) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.PLAID_DATA, value: { - loading: false, + isLoading: false, error: '', }, }], @@ -48,7 +48,7 @@ function openPlaidBankAccountSelector(publicToken, bankName, allowDebit) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.PLAID_DATA, value: { - loading: false, + isLoading: false, error: Localize.translateLocal('bankAccount.error.noBankAccountAvailable'), }, }], diff --git a/src/libs/actions/ReimbursementAccount/errors.js b/src/libs/actions/ReimbursementAccount/errors.js index 9c4b53f5ffef..8d404251b448 100644 --- a/src/libs/actions/ReimbursementAccount/errors.js +++ b/src/libs/actions/ReimbursementAccount/errors.js @@ -1,15 +1,6 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../../ONYXKEYS'; - -/** - * Show error modal and optionally a specific error message - * - * @param {String} errorModalMessage The error message to be displayed in the modal's body. - * @param {Boolean} isErrorModalMessageHtml if @errorModalMessage is in html format or not - */ -function showBankAccountErrorModal(errorModalMessage = null, isErrorModalMessageHtml = false) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorModalMessage, isErrorModalMessageHtml}); -} +import DateUtils from '../../DateUtils'; /** * Set the current fields with errors. @@ -24,12 +15,12 @@ function setPersonalBankAccountFormValidationErrorFields(errorFields) { /** * Set the current fields with errors. * - * @param {String} errors + * @param {Object} errorFields */ -function setBankAccountFormValidationErrors(errors) { +function setBankAccountFormValidationErrors(errorFields) { // We set 'errors' to null first because we don't have a way yet to replace a specific property like 'errors' without merging it - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields}); } /** @@ -37,7 +28,7 @@ function setBankAccountFormValidationErrors(errors) { */ function resetReimbursementAccount() { setBankAccountFormValidationErrors({}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {successRoute: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); } /** @@ -46,11 +37,15 @@ function resetReimbursementAccount() { * @param {String} error */ function showBankAccountFormValidationError(error) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { + // eslint-disable-next-line rulesdir/prefer-localization + errors: { + [DateUtils.getMicroseconds()]: error, + }, + }); } export { - showBankAccountErrorModal, setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, showBankAccountFormValidationError, diff --git a/src/libs/actions/ReimbursementAccount/fetchFreePlanVerifiedBankAccount.js b/src/libs/actions/ReimbursementAccount/fetchFreePlanVerifiedBankAccount.js index 33d6fef990a8..11e02395d761 100644 --- a/src/libs/actions/ReimbursementAccount/fetchFreePlanVerifiedBankAccount.js +++ b/src/libs/actions/ReimbursementAccount/fetchFreePlanVerifiedBankAccount.js @@ -13,7 +13,7 @@ import BankAccount from '../../models/BankAccount'; * @returns {Object} */ function getInitialData(localBankAccountState) { - const initialData = {loading: true, error: ''}; + const initialData = {isLoading: true, error: ''}; // Some UI needs to know the bank account state during the loading process, so we are keeping it in Onyx if passed if (localBankAccountState) { @@ -73,7 +73,7 @@ function fetchNameValuePairsAndBankAccount() { }; }) .finally(() => { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading: false}); }); } @@ -187,7 +187,12 @@ function fetchFreePlanVerifiedBankAccount(stepToOpen, localBankAccountState) { throttledDate, maxAttemptsReached, error: '', + isLoading: false, + }); + Onyx.merge(ONYXKEYS.PLAID_DATA, { isPlaidDisabled, + error: '', + isLoading: false, }); }); } diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js index 50bbae591692..5bc9e3ea562a 100644 --- a/src/libs/actions/ReimbursementAccount/index.js +++ b/src/libs/actions/ReimbursementAccount/index.js @@ -7,7 +7,6 @@ import deleteFromBankAccountList from './deleteFromBankAccountList'; export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute} from './navigation'; export { - showBankAccountErrorModal, setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, resetReimbursementAccount, diff --git a/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js b/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js index 97a2499376c5..d5c24491e551 100644 --- a/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js +++ b/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js @@ -35,7 +35,7 @@ function getBankAccountListAndGoToValidateStep(updatedACHData) { achData.bankAccountInReview = achData.state === BankAccount.STATE.VERIFYING; navigation.goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.VALIDATION, achData); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading: false}); }); } @@ -73,19 +73,15 @@ function getNextStep(updatedACHData) { */ function showSetupWithdrawalAccountErrors(response, verificationsError, updatedACHData) { let error = verificationsError; - let isErrorHTML = false; const responseACHData = lodashGet(response, 'achData', {}); if (response.jsonCode === 666 || response.jsonCode === 404) { - // Since these specific responses can have an error message in html format with richer content, give priority to the html error. - error = response.htmlMessage || response.message; - isErrorHTML = Boolean(response.htmlMessage); + error = response.message; } if (response.jsonCode === 402) { if (hasAccountOrRoutingError(response)) { errors.setBankAccountFormValidationErrors({routingNumber: true}); - errors.showBankAccountErrorModal(); } else if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_INCORPORATION_STATE) { error = Localize.translateLocal('bankAccount.error.incorporationState'); } else if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_INCORPORATION_TYPE) { @@ -97,7 +93,6 @@ function showSetupWithdrawalAccountErrors(response, verificationsError, updatedA if (error) { errors.showBankAccountFormValidationError(error); - errors.showBankAccountErrorModal(error, isErrorHTML); } const nextStep = response.jsonCode === 200 && !error ? getNextStep(updatedACHData) : updatedACHData.currentStep; @@ -105,7 +100,7 @@ function showSetupWithdrawalAccountErrors(response, verificationsError, updatedA ...responseACHData, subStep: hasAccountOrRoutingError(response) ? CONST.BANK_ACCOUNT.SUBSTEP.MANUAL : responseACHData.subStep, }); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading: false}); } /** @@ -193,7 +188,7 @@ function mergeParamsWithLocalACHData(data) { * @param {Array} [params.beneficialOwners] */ function setupWithdrawalAccount(params) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: true, error: '', errors: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading: true, error: '', errors: null}); const updatedACHData = mergeParamsWithLocalACHData(params); DeprecatedAPI.BankAccount_SetupWithdrawal(updatedACHData) .then((response) => { @@ -225,7 +220,7 @@ function setupWithdrawalAccount(params) { ...(_.omit(responseACHData, 'nextStepValues')), ...responseACHData.nextStepValues, }); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading: false}); return; } @@ -241,12 +236,12 @@ function setupWithdrawalAccount(params) { } else { navigation.goToWithdrawalAccountSetupStep(nextStep, responseACHData); } - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading: false}); }) .catch((response) => { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false, achData: {...updatedACHData}}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading: false, achData: {...updatedACHData}}); console.error(response.stack); - errors.showBankAccountErrorModal(Localize.translateLocal('common.genericErrorMessage')); + errors.showBankAccountFormValidationError('common.genericErrorMessage'); }); } diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js index eed475161531..17bc7cea68a2 100644 --- a/src/pages/AddPersonalBankAccountPage.js +++ b/src/pages/AddPersonalBankAccountPage.js @@ -33,7 +33,7 @@ const propTypes = { personalBankAccount: PropTypes.shape({ error: PropTypes.string, shouldShowSuccess: PropTypes.bool, - loading: PropTypes.bool, + isLoading: PropTypes.bool, }), }; @@ -41,7 +41,7 @@ const defaultProps = { personalBankAccount: { error: '', shouldShowSuccess: false, - loading: false, + isLoading: false, }, }; @@ -124,7 +124,7 @@ class AddPersonalBankAccountPage extends React.Component { render() { const shouldShowSuccess = lodashGet(this.props, 'personalBankAccount.shouldShowSuccess', false); const error = lodashGet(this.props, 'personalBankAccount.error', ''); - const loading = lodashGet(this.props, 'personalBankAccount.loading', false); + const isLoading = lodashGet(this.props, 'personalBankAccount.isLoading', false); return ( @@ -197,7 +197,7 @@ class AddPersonalBankAccountPage extends React.Component { buttonText={this.props.translate('common.saveAndContinue')} onSubmit={this.submit} message={error} - isLoading={loading} + isLoading={isLoading} /> )} diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js new file mode 100644 index 000000000000..d6226a180ca9 --- /dev/null +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -0,0 +1,171 @@ +import _ from 'underscore'; +import React from 'react'; +import {Image, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import CONST from '../../CONST'; +import * as BankAccounts from '../../libs/actions/BankAccounts'; +import Navigation from '../../libs/Navigation/Navigation'; +import Text from '../../components/Text'; +import TextInput from '../../components/TextInput'; +import styles from '../../styles/styles'; +import CheckboxWithLabel from '../../components/CheckboxWithLabel'; +import TextLink from '../../components/TextLink'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import * as ValidationUtils from '../../libs/ValidationUtils'; +import compose from '../../libs/compose'; +import ONYXKEYS from '../../ONYXKEYS'; +import * as ReimbursementAccount from '../../libs/actions/ReimbursementAccount'; +import exampleCheckImage from './exampleCheckImage'; +import ReimbursementAccountForm from './ReimbursementAccountForm'; +import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils'; + +const propTypes = { + ...withLocalizePropTypes, +}; + +class BankAccountManualStep extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + this.clearErrorAndSetValue = this.clearErrorAndSetValue.bind(this); + this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey); + + this.state = { + hasAcceptedTerms: ReimbursementAccountUtils.getDefaultStateForField(props, 'acceptTerms', true), + routingNumber: ReimbursementAccountUtils.getDefaultStateForField(props, 'routingNumber'), + accountNumber: ReimbursementAccountUtils.getDefaultStateForField(props, 'accountNumber'), + }; + + // Map a field to the key of the error's translation + this.errorTranslationKeys = { + routingNumber: 'bankAccount.error.routingNumber', + accountNumber: 'bankAccount.error.accountNumber', + hasAcceptedTerms: 'common.error.acceptedTerms', + }; + } + + /** + * @returns {Boolean} + */ + validate() { + const errorFields = {}; + const routingNumber = this.state.routingNumber.trim(); + + if (!CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(this.state.accountNumber.trim())) { + errorFields.accountNumber = true; + } + if (!CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(routingNumber) || !ValidationUtils.isValidRoutingNumber(routingNumber)) { + errorFields.routingNumber = true; + } + if (!this.state.hasAcceptedTerms) { + errorFields.hasAcceptedTerms = true; + } + + ReimbursementAccount.setBankAccountFormValidationErrors(errorFields); + + return _.size(errorFields) === 0; + } + + submit() { + if (!this.validate()) { + return; + } + + const params = { + bankAccountID: ReimbursementAccountUtils.getDefaultStateForField(this.props, 'bankAccountID', 0), + mask: ReimbursementAccountUtils.getDefaultStateForField(this.props, 'plaidMask'), + bankName: ReimbursementAccountUtils.getDefaultStateForField(this.props, 'bankName'), + plaidAccountID: ReimbursementAccountUtils.getDefaultStateForField(this.props, 'plaidAccountID'), + ...this.state, + }; + BankAccounts.setupWithdrawalAccount(params); + } + + /** + * @param {String} inputKey + * @param {String} value + */ + clearErrorAndSetValue(inputKey, value) { + const newState = {[inputKey]: value}; + this.setState(newState); + ReimbursementAccount.updateReimbursementAccountDraft(newState); + ReimbursementAccountUtils.clearError(this.props, inputKey); + } + + render() { + const shouldDisableInputs = Boolean(ReimbursementAccountUtils.getDefaultStateForField(this.props, 'bankAccountID')); + + return ( + <> + BankAccounts.setBankAccountSubStep(null)} + onCloseButtonPress={Navigation.dismissModal} + /> + + + {this.props.translate('bankAccount.checkHelpLine')} + + + this.clearErrorAndSetValue('routingNumber', value)} + disabled={shouldDisableInputs} + errorText={this.getErrorText('routingNumber')} + /> + this.clearErrorAndSetValue('accountNumber', value)} + disabled={shouldDisableInputs} + errorText={this.getErrorText('accountNumber')} + /> + this.clearErrorAndSetValue('hasAcceptedTerms', value)} + LabelComponent={() => ( + + + {this.props.translate('common.iAcceptThe')} + + + {`Expensify ${this.props.translate('common.termsOfService')}`} + + + )} + errorText={this.getErrorText('hasAcceptedTerms')} + /> + + + ); + } +} + +BankAccountManualStep.propTypes = propTypes; +export default compose( + withLocalize, + withOnyx({ + // Needed, to retrieve errorFields + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, + reimbursementAccountDraft: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + }, + }), +)(BankAccountManualStep); diff --git a/src/pages/ReimbursementAccount/BankAccountPlaidStep.js b/src/pages/ReimbursementAccount/BankAccountPlaidStep.js new file mode 100644 index 000000000000..f738aecf7d8d --- /dev/null +++ b/src/pages/ReimbursementAccount/BankAccountPlaidStep.js @@ -0,0 +1,102 @@ +import _ from 'underscore'; +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import CONST from '../../CONST'; +import * as BankAccounts from '../../libs/actions/BankAccounts'; +import Navigation from '../../libs/Navigation/Navigation'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import compose from '../../libs/compose'; +import ONYXKEYS from '../../ONYXKEYS'; +import AddPlaidBankAccount from '../../components/AddPlaidBankAccount'; +import * as ReimbursementAccount from '../../libs/actions/ReimbursementAccount'; +import ReimbursementAccountForm from './ReimbursementAccountForm'; +import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils'; + +const propTypes = { + /** The OAuth URI + stateID needed to re-initialize the PlaidLink after the user logs into their bank */ + receivedRedirectURI: PropTypes.string, + + /** During the OAuth flow we need to use the plaidLink token that we initially connected with */ + plaidLinkOAuthToken: PropTypes.string, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + receivedRedirectURI: null, + plaidLinkOAuthToken: '', +}; + +class BankAccountPlaidStep extends React.Component { + constructor(props) { + super(props); + this.submit = this.submit.bind(this); + } + + submit() { + const selectedPlaidBankAccount = this.props.plaidData.selectedPlaidBankAccount; + if (!selectedPlaidBankAccount) { + return; + } + + ReimbursementAccount.updateReimbursementAccountDraft({ + routingNumber: selectedPlaidBankAccount.routingNumber, + accountNumber: selectedPlaidBankAccount.accountNumber, + plaidMask: selectedPlaidBankAccount.mask, + isSavings: selectedPlaidBankAccount.isSavings, + bankName: selectedPlaidBankAccount.bankName, + plaidAccountID: selectedPlaidBankAccount.plaidAccountID, + plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken, + }); + + const bankAccountID = ReimbursementAccountUtils.getDefaultStateForField(this.props, 'bankAccountID', 0); + BankAccounts.connectBankAccountWithPlaid(bankAccountID, selectedPlaidBankAccount); + } + + render() { + const bankAccountID = ReimbursementAccountUtils.getDefaultStateForField(this.props, 'bankAccountID', 0); + + return ( + <> + BankAccounts.setBankAccountSubStep(null)} + onCloseButtonPress={Navigation.dismissModal} + /> + + { + BankAccounts.updatePlaidData({selectedPlaidBankAccount: params.selectedPlaidBankAccount}); + }} + onExitPlaid={() => BankAccounts.setBankAccountSubStep(null)} + receivedRedirectURI={this.props.receivedRedirectURI} + plaidLinkOAuthToken={this.props.plaidLinkOAuthToken} + allowDebit + bankAccountID={bankAccountID} + /> + + + ); + } +} + +BankAccountPlaidStep.propTypes = propTypes; +BankAccountPlaidStep.defaultProps = defaultProps; +export default compose( + withLocalize, + withOnyx({ + reimbursementAccountDraft: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + }, + plaidData: { + key: ONYXKEYS.PLAID_DATA, + }, + }), +)(BankAccountPlaidStep); diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index a38a8b915403..c2f64a4e22a5 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -1,9 +1,9 @@ -import _ from 'underscore'; import React from 'react'; -import {View, Image, ScrollView} from 'react-native'; +import {View, ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; -import lodashGet from 'lodash/get'; +import BankAccountManualStep from './BankAccountManualStep'; +import BankAccountPlaidStep from './BankAccountPlaidStep'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import MenuItem from '../../components/MenuItem'; import * as Expensicons from '../../components/Icon/Expensicons'; @@ -13,33 +13,19 @@ import Icon from '../../components/Icon'; import colors from '../../styles/colors'; import Navigation from '../../libs/Navigation/Navigation'; import CONST from '../../CONST'; -import AddPlaidBankAccount from '../../components/AddPlaidBankAccount'; -import CheckboxWithLabel from '../../components/CheckboxWithLabel'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import exampleCheckImage from './exampleCheckImage'; -import TextInput from '../../components/TextInput'; import Text from '../../components/Text'; import * as BankAccounts from '../../libs/actions/BankAccounts'; import ONYXKEYS from '../../ONYXKEYS'; import compose from '../../libs/compose'; -import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils'; -import ReimbursementAccountForm from './ReimbursementAccountForm'; -import reimbursementAccountPropTypes from './reimbursementAccountPropTypes'; import Section from '../../components/Section'; -import * as ValidationUtils from '../../libs/ValidationUtils'; import * as Illustrations from '../../components/Icon/Illustrations'; import getPlaidDesktopMessage from '../../libs/getPlaidDesktopMessage'; import CONFIG from '../../CONFIG'; import ROUTES from '../../ROUTES'; import Button from '../../components/Button'; -import FormScrollView from '../../components/FormScrollView'; -import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; const propTypes = { - /** Bank account currently in setup */ - // eslint-disable-next-line react/no-unused-prop-types - reimbursementAccount: reimbursementAccountPropTypes.isRequired, - /** The OAuth URI + stateID needed to re-initialize the PlaidLink after the user logs into their bank */ receivedRedirectURI: PropTypes.string, @@ -61,310 +47,117 @@ const defaultProps = { user: {}, }; -class BankAccountStep extends React.Component { - constructor(props) { - super(props); - - this.toggleTerms = this.toggleTerms.bind(this); - this.addManualAccount = this.addManualAccount.bind(this); - this.addPlaidAccount = this.addPlaidAccount.bind(this); - this.state = { - selectedPlaidBankAccount: undefined, - hasAcceptedTerms: ReimbursementAccountUtils.getDefaultStateForField(props, 'acceptTerms', true), - routingNumber: ReimbursementAccountUtils.getDefaultStateForField(props, 'routingNumber'), - accountNumber: ReimbursementAccountUtils.getDefaultStateForField(props, 'accountNumber'), - }; - - // Keys in this.errorTranslationKeys are associated to inputs, they are a subset of the keys found in this.state - this.errorTranslationKeys = { - routingNumber: 'bankAccount.error.routingNumber', - accountNumber: 'bankAccount.error.accountNumber', - hasAcceptedTerms: 'common.error.acceptedTerms', - }; - - this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, this.errorTranslationKeys, inputKey); - this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); - this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); - } - - toggleTerms() { - this.setState((prevState) => { - const hasAcceptedTerms = !prevState.hasAcceptedTerms; - BankAccounts.updateReimbursementAccountDraft({acceptTerms: hasAcceptedTerms}); - return {hasAcceptedTerms}; - }); - this.clearError('hasAcceptedTerms'); - } - - /** - * @returns {Boolean} - */ - validate() { - const errors = {}; - - if (!CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(this.state.accountNumber.trim())) { - errors.accountNumber = true; - } - if (!CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(this.state.routingNumber.trim()) || !ValidationUtils.isValidRoutingNumber(this.state.routingNumber.trim())) { - errors.routingNumber = true; - } - if (!this.state.hasAcceptedTerms) { - errors.hasAcceptedTerms = true; - } - - BankAccounts.setBankAccountFormValidationErrors(errors); - return _.size(errors) === 0; - } - - /** - * Clear the error associated to inputKey if found and store the inputKey new value in the state. - * - * @param {String} inputKey - * @param {String} value - */ - clearErrorAndSetValue(inputKey, value) { - const newState = {[inputKey]: value}; - this.setState(newState); - BankAccounts.updateReimbursementAccountDraft(newState); - this.clearError(inputKey); - } - - addManualAccount() { - if (!this.validate()) { - return; - } - - BankAccounts.setupWithdrawalAccount({ - acceptTerms: this.state.hasAcceptedTerms, - accountNumber: this.state.accountNumber, - routingNumber: this.state.routingNumber, - setupType: CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL, +const BankAccountStep = (props) => { + const shouldReinitializePlaidLink = props.plaidLinkOAuthToken && props.receivedRedirectURI && props.achData.subStep !== CONST.BANK_ACCOUNT.SUBSTEP.MANUAL; + const subStep = shouldReinitializePlaidLink ? CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID : props.achData.subStep; + const plaidDesktopMessage = getPlaidDesktopMessage(); + const bankAccountRoute = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.BANK_ACCOUNT}`; - // Note: These are hardcoded as we're not supporting AU bank accounts for the free plan - country: CONST.COUNTRY.US, - currency: CONST.CURRENCY.USD, - fieldsType: CONST.BANK_ACCOUNT.FIELDS_TYPE.LOCAL, - }); + if (subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL) { + return ; } - /** - * Add the Bank account retrieved via Plaid in db - */ - addPlaidAccount() { - const selectedPlaidBankAccount = this.state.selectedPlaidBankAccount; - if (!this.state.selectedPlaidBankAccount) { - return; - } - BankAccounts.setupWithdrawalAccount({ - acceptTerms: true, - setupType: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID, - - // Params passed via the Plaid callback when an account is selected - plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken, - accountNumber: selectedPlaidBankAccount.accountNumber, - routingNumber: selectedPlaidBankAccount.routingNumber, - plaidAccountID: selectedPlaidBankAccount.plaidAccountID, - ownershipType: selectedPlaidBankAccount.ownershipType, - isSavings: selectedPlaidBankAccount.isSavings, - bankName: selectedPlaidBankAccount.bankName, - addressName: selectedPlaidBankAccount.addressName, - mask: selectedPlaidBankAccount.mask, - - // Note: These are hardcoded as we're not supporting AU bank accounts for the free plan - country: CONST.COUNTRY.US, - currency: CONST.CURRENCY.USD, - fieldsType: CONST.BANK_ACCOUNT.FIELDS_TYPE.LOCAL, - }); + if (subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID) { + return ; } - render() { - // Disable bank account fields once they've been added in db so they can't be changed - const isFromPlaid = this.props.achData.setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID; - const shouldDisableInputs = Boolean(this.props.achData.bankAccountID) || isFromPlaid; - const shouldReinitializePlaidLink = this.props.plaidLinkOAuthToken && this.props.receivedRedirectURI && this.props.achData.subStep !== CONST.BANK_ACCOUNT.SUBSTEP.MANUAL; - const subStep = shouldReinitializePlaidLink ? CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID : this.props.achData.subStep; - const plaidDesktopMessage = getPlaidDesktopMessage(); - const bankAccountRoute = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.BANK_ACCOUNT}`; - const error = lodashGet(this.props, 'reimbursementAccount.error', ''); - const loading = lodashGet(this.props, 'reimbursementAccount.loading', false); - const validated = lodashGet(this.props, 'user.validated', false); - return ( - - { - // If we have a subStep then we will remove otherwise we will go back - if (subStep) { - BankAccounts.setBankAccountSubStep(null); - return; - } - Navigation.goBack(); - }} - shouldShowGetAssistanceButton - guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BANK_ACCOUNT} - shouldShowBackButton + return ( + + { + // If we have a subStep then we will remove otherwise we will go back + if (subStep) { + BankAccounts.setBankAccountSubStep(null); + return; + } + Navigation.goBack(); + }} + shouldShowGetAssistanceButton + guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BANK_ACCOUNT} + shouldShowBackButton + /> + +
- {!subStep && ( - -
- - {this.props.translate('bankAccount.toGetStarted')} - - {plaidDesktopMessage && ( - - - {this.props.translate(plaidDesktopMessage)} - - - )} -