diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index c450642df1c8..ad8e10a31c70 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -9,6 +9,7 @@ import CONST from '../CONST'; import withWindowDimensions from './withWindowDimensions'; import Permissions from '../libs/Permissions'; import PopoverMenu from './PopoverMenu'; +import * as BankAccounts from '../libs/actions/BankAccounts'; const propTypes = { isVisible: PropTypes.bool.isRequired, @@ -47,7 +48,10 @@ const AddPaymentMethodMenu = props => ( { text: props.translate('common.bankAccount'), icon: Expensicons.Bank, - onSelected: () => props.onItemSelected(CONST.PAYMENT_METHODS.BANK_ACCOUNT), + onSelected: () => { + BankAccounts.clearPlaid(); + props.onItemSelected(CONST.PAYMENT_METHODS.BANK_ACCOUNT); + }, }, ...(Permissions.canUseWallet(props.betas) ? [{ text: props.translate('common.debitCard'), diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index c0f926430b0c..96fa3dee7559 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,21 +77,14 @@ class AddPlaidBankAccount extends React.Component { this.selectAccount = this.selectAccount.bind(this); this.getPlaidLinkToken = this.getPlaidLinkToken.bind(this); - - this.state = { - selectedIndex: undefined, - institution: {}, - }; } componentDidMount() { // If we're coming from Plaid OAuth flow then we need to reuse the existing plaidLinkToken - // Otherwise, clear the existing token and fetch a new one - if (this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) { + if ((this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) || !_.isEmpty(this.props.plaidData)) { return; } - BankAccounts.clearPlaid(); BankAccounts.openPlaidBankLogin(this.props.allowDebit, this.props.bankAccountID); } @@ -119,30 +112,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 selectedPlaidBankAccount = 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 +152,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 +180,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..2b2a63516b09 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,10 @@ function clearPlaid() { Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, ''); } +function updatePlaidData(plaidData) { + Onyx.merge(ONYXKEYS.PLAID_DATA, plaidData); +} + /** * Helper method to build the Onyx data required during setup of a Verified Business Bank Account * @@ -86,6 +88,27 @@ function getVBBADataForOnyx() { }; } +/** + * Submit Bank Account step with Plaid data so php can perform some checks. + * + * @param {Number} bankAccountID + * @param {Object} selectedPlaidBankAccount + */ +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, getVBBADataForOnyx()); +} + /** * Adds a bank account via Plaid * @@ -114,7 +137,7 @@ function addPersonalBankAccount(account, password) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, value: { - loading: true, + isLoading: true, error: '', }, }, @@ -124,7 +147,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 +158,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 +222,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..3427524711c2 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,11 @@ function setPersonalBankAccountFormValidationErrorFields(errorFields) { /** * Set the current fields with errors. * - * @param {String} errors + * @param {Object} errorFields */ -function setBankAccountFormValidationErrors(errors) { - // 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}); +function setBankAccountFormValidationErrors(errorFields) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields}); } /** @@ -37,7 +27,7 @@ function setBankAccountFormValidationErrors(errors) { */ function resetReimbursementAccount() { setBankAccountFormValidationErrors({}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {successRoute: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); } /** @@ -46,11 +36,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..ccdf00b7d518 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(Localize.translateLocal('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/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index d5906c4cf136..739b72236261 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -28,6 +28,7 @@ import walletAdditionalDetailsDraftPropTypes from './walletAdditionalDetailsDraf import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../components/withCurrentUserPersonalDetails'; import * as PersonalDetails from '../../libs/actions/PersonalDetails'; import OfflineIndicator from '../../components/OfflineIndicator'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const propTypes = { ...withLocalizePropTypes, @@ -262,12 +263,10 @@ class AdditionalDetailsStep extends React.Component { ); } - const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); - const isErrorVisible = _.size(this.getErrors()) > 0 - || !_.isEmpty(errors); + const errorMessage = ErrorUtils.getLatestErrorMessage(this.props.walletAdditionalDetails) || ''; + const isErrorVisible = _.size(this.getErrors()) > 0 || Boolean(errorMessage); const shouldAskForFullSSN = this.props.walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN; const {firstName, lastName} = PersonalDetails.extractFirstAndLastNameFromAvailableDetails(this.props.currentUserPersonalDetails); - const errorMessage = _.isEmpty(errors) ? '' : _.last(_.values(errors)); return ( diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index c4368c8cb9c6..84aacf6f7ae5 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -5,7 +5,6 @@ import { View, } from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import lodashGet from 'lodash/get'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -17,6 +16,7 @@ import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButto import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import OfflineIndicator from '../../components/OfflineIndicator'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -142,9 +142,7 @@ class IdologyQuestions extends React.Component { }; })); - const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); - const isErrorVisible = this.state.errorMessage || !_.isEmpty(errors); - const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); + const errorMessage = ErrorUtils.getLatestErrorMessage(this.props.walletAdditionalDetails) || this.state.errorMessage; return ( @@ -168,7 +166,7 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); diff --git a/src/pages/EnablePayments/OnfidoPrivacy.js b/src/pages/EnablePayments/OnfidoPrivacy.js index 921d85e14c63..34f96b4f585a 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.js +++ b/src/pages/EnablePayments/OnfidoPrivacy.js @@ -15,6 +15,7 @@ import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButto import FormScrollView from '../../components/FormScrollView'; import walletAdditionalDetailsDraftPropTypes from './walletAdditionalDetailsDraftPropTypes'; import walletOnfidoDataPropTypes from './walletOnfidoDataPropTypes'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const propTypes = { /** Stores various information used to build the UI and call any APIs */ @@ -53,8 +54,7 @@ class OnfidoPrivacy extends React.Component { } render() { - const errors = lodashGet(this.props, 'walletOnfidoData.errors', {}); - let onfidoError = _.isEmpty(errors) ? '' : _.last(_.values(errors)); + let onfidoError = ErrorUtils.getLatestErrorMessage(this.props.walletOnfidoData) || ''; const onfidoFixableErrors = lodashGet(this.props, 'walletOnfidoData.fixableErrors', []); onfidoError += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; diff --git a/src/pages/EnablePayments/TermsStep.js b/src/pages/EnablePayments/TermsStep.js index 2c5161dbc70e..0a6d94d10324 100644 --- a/src/pages/EnablePayments/TermsStep.js +++ b/src/pages/EnablePayments/TermsStep.js @@ -1,8 +1,6 @@ import React from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import lodashGet from 'lodash/get'; -import _ from 'underscore'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; @@ -17,6 +15,7 @@ import ShortTermsForm from './TermsPage/ShortTermsForm'; import LongTermsForm from './TermsPage/LongTermsForm'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import walletTermsPropTypes from './walletTermsPropTypes'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const propTypes = { /** Comes from Onyx. Information about the terms for the wallet */ @@ -61,8 +60,7 @@ class TermsStep extends React.Component { } render() { - const errors = lodashGet(this.props, 'walletTerms.errors', {}); - const errorMessage = this.state.error ? this.props.translate('common.error.acceptedTerms') : (_.last(_.values(errors)) || ''); + const errorMessage = this.state.error ? this.props.translate('common.error.acceptedTerms') : (ErrorUtils.getLatestErrorMessage(this.props.walletTerms) || ''); return ( <> diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js new file mode 100644 index 000000000000..c291af8f0a2f --- /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 = { + acceptTerms: 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', + acceptTerms: '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.acceptTerms) { + errorFields.acceptTerms = 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('acceptTerms', value)} + LabelComponent={() => ( + + + {this.props.translate('common.iAcceptThe')} + + + {`Expensify ${this.props.translate('common.termsOfService')}`} + + + )} + errorText={this.getErrorText('acceptTerms')} + /> + + + ); + } +} + +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)} - - - )} -