From 687cac64d604030134ce4c6db89f03590eab5173 Mon Sep 17 00:00:00 2001 From: Nathalie Kuoch Date: Thu, 1 Dec 2022 15:31:38 +0100 Subject: [PATCH 01/28] Refactor VBBA setup flow Allow to go back from offline view Fix and refactor startOver modal Refactor getDefaultStateForField so people dont think reimbursementAccountDraft is not used --- config/webpack/webpack.dev.js | 1 + src/ONYXKEYS.js | 3 + src/components/AddPlaidBankAccount.js | 21 +- .../ReimbursementAccountLoadingIndicator.js | 12 +- src/libs/ReimbursementAccountUtils.js | 24 +- src/libs/actions/BankAccounts.js | 12 +- src/libs/actions/PaymentMethods.js | 16 -- src/libs/actions/Plaid.js | 12 +- .../actions/ReimbursementAccount/index.js | 6 + .../ReimbursementAccount/navigation.js | 86 +------ .../resetFreePlanBankAccount.js | 16 +- src/libs/getPlaidLinkTokenParameters/index.js | 3 +- src/pages/AddPersonalBankAccountPage.js | 11 +- .../ReimbursementAccount/ACHContractStep.js | 58 ++--- .../BankAccountManualStep.js | 65 +++--- .../BankAccountPlaidStep.js | 51 +++-- .../ReimbursementAccount/BankAccountStep.js | 211 +++++++++--------- src/pages/ReimbursementAccount/CompanyStep.js | 81 ++++--- .../ContinueBankAccountSetup.js | 3 +- src/pages/ReimbursementAccount/EnableStep.js | 16 +- .../ReimbursementAccountForm.js | 21 +- .../ReimbursementAccountPage.js | 189 +++++++++++----- .../RequestorOnfidoStep.js | 27 ++- .../ReimbursementAccount/RequestorStep.js | 79 +++---- .../ReimbursementAccount/ValidationStep.js | 30 +-- .../plaidDataPropTypes.js | 19 +- .../reimbursementAccountPropTypes.js | 17 +- .../workspace/WorkspacePageWithSections.js | 6 +- .../WorkspaceResetBankAccountModal.js | 45 +--- .../reimburse/WorkspaceReimburseSection.js | 4 +- .../reimburse/WorkspaceReimburseView.js | 6 +- 31 files changed, 571 insertions(+), 580 deletions(-) diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js index 1cd8367731f9..72ca1ed2d5a6 100644 --- a/config/webpack/webpack.dev.js +++ b/config/webpack/webpack.dev.js @@ -41,6 +41,7 @@ module.exports = (env = {}) => portfinder.getPortPromise({port: BASE_PORT}) ...proxySettings, historyApiFallback: true, port, + https: true, }, plugins: [ new DefinePlugin({ diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 60a686195dcd..969c1367c7b0 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -79,6 +79,9 @@ export default { // Plaid data (access tokens, bank accounts ...) PLAID_DATA: 'plaidData', + // If we disabled Plaid because of too many attempts + IS_PLAID_DISABLED: 'isPlaidDisabled', + // Token needed to initialize Plaid link PLAID_LINK_TOKEN: 'plaidLinkToken', diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 3aa31b8e7d31..cd23be0ca442 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -5,7 +5,6 @@ import { View, } from 'react-native'; import PropTypes from 'prop-types'; -import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; import Log from '../libs/Log'; import PlaidLink from './PlaidLink'; @@ -16,7 +15,7 @@ import themeColors from '../styles/themes/default'; import compose from '../libs/compose'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import Picker from './Picker'; -import plaidDataPropTypes from '../pages/ReimbursementAccount/plaidDataPropTypes'; +import {plaidDataPropTypes} from '../pages/ReimbursementAccount/plaidDataPropTypes'; import Text from './Text'; import getBankIcon from './Icon/BankIcons'; import Icon from './Icon'; @@ -24,7 +23,7 @@ import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlocking const propTypes = { /** Contains plaid data */ - plaidData: plaidDataPropTypes, + plaidData: plaidDataPropTypes.isRequired, /** Selected account ID from the Picker associated with the end of the Plaid flow */ selectedPlaidAccountID: PropTypes.string, @@ -57,13 +56,6 @@ const propTypes = { }; const defaultProps = { - plaidData: { - bankName: '', - plaidAccessToken: '', - bankAccounts: [], - isLoading: false, - error: '', - }, selectedPlaidAccountID: '', plaidLinkToken: '', onExitPlaid: () => {}, @@ -84,7 +76,7 @@ class AddPlaidBankAccount extends React.Component { componentDidMount() { // If we're coming from Plaid OAuth flow then we need to reuse the existing plaidLinkToken - if ((this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) || !_.isEmpty(this.props.plaidData)) { + if ((this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) || !_.isEmpty(this.props.plaidData.bankAccounts)) { return; } @@ -105,7 +97,7 @@ class AddPlaidBankAccount extends React.Component { } render() { - const plaidBankAccounts = lodashGet(this.props.plaidData, 'bankAccounts', []); + const plaidBankAccounts = this.props.plaidData.bankAccounts || []; const token = this.getPlaidLinkToken(); const options = _.map(plaidBankAccounts, account => ({ value: account.plaidAccountID, @@ -117,7 +109,7 @@ class AddPlaidBankAccount extends React.Component { if (!plaidBankAccounts.length) { return ( - {(!token || this.props.plaidData.isLoading) + {this.props.plaidData.isLoading && ( @@ -187,9 +179,6 @@ AddPlaidBankAccount.defaultProps = defaultProps; export default compose( withLocalize, withOnyx({ - plaidData: { - key: ONYXKEYS.PLAID_DATA, - }, plaidLinkToken: { key: ONYXKEYS.PLAID_LINK_TOKEN, initWithStoredValues: false, diff --git a/src/components/ReimbursementAccountLoadingIndicator.js b/src/components/ReimbursementAccountLoadingIndicator.js index c472bb3ae0a4..73c01a1f956d 100644 --- a/src/components/ReimbursementAccountLoadingIndicator.js +++ b/src/components/ReimbursementAccountLoadingIndicator.js @@ -10,11 +10,13 @@ import Navigation from '../libs/Navigation/Navigation'; import ScreenWrapper from './ScreenWrapper'; import FullScreenLoadingIndicator from './FullscreenLoadingIndicator'; import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlockingView'; +import compose from '../libs/compose'; +import {withNetwork} from './OnyxProvider'; const propTypes = { /** Whether the user is submitting verifications data */ isSubmittingVerificationsData: PropTypes.bool.isRequired, - + onBackButtonPress: PropTypes.func.isRequired, ...withLocalizePropTypes, }; @@ -23,6 +25,8 @@ const ReimbursementAccountLoadingIndicator = props => ( {props.isSubmittingVerificationsData ? ( @@ -49,4 +53,8 @@ const ReimbursementAccountLoadingIndicator = props => ( ReimbursementAccountLoadingIndicator.propTypes = propTypes; ReimbursementAccountLoadingIndicator.displayName = 'ReimbursementAccountLoadingIndicator'; -export default withLocalize(ReimbursementAccountLoadingIndicator); +export default compose( + withLocalize, + withNetwork(), +)(ReimbursementAccountLoadingIndicator); + diff --git a/src/libs/ReimbursementAccountUtils.js b/src/libs/ReimbursementAccountUtils.js index 4074f1e8ab6c..6c11b72fb5c4 100644 --- a/src/libs/ReimbursementAccountUtils.js +++ b/src/libs/ReimbursementAccountUtils.js @@ -1,4 +1,3 @@ -import _ from 'underscore'; import lodashGet from 'lodash/get'; import * as BankAccounts from './actions/BankAccounts'; import FormHelper from './FormHelper'; @@ -15,28 +14,16 @@ const clearErrors = (props, paths) => formHelper.clearErrors(props, paths); /** * Get the default state for input fields in the VBA flow * - * @param {Object} props + * @param {Object} reimbursementAccountDraft + * @param {Object} reimbursementAccount * @param {String} fieldName * @param {*} defaultValue * * @returns {*} */ -function getDefaultStateForField(props, fieldName, defaultValue = '') { - return lodashGet(props, ['reimbursementAccountDraft', fieldName]) - || lodashGet(props, ['reimbursementAccount', 'achData', fieldName], defaultValue); -} - -/** - * @param {Object} props - * @param {Array} fieldNames - * - * @returns {*} - */ -function getBankAccountFields(props, fieldNames) { - return { - ..._.pick(lodashGet(props, 'reimbursementAccount.achData'), ...fieldNames), - ..._.pick(props.reimbursementAccountDraft, ...fieldNames), - }; +function getDefaultStateForField(reimbursementAccountDraft, reimbursementAccount, fieldName, defaultValue = '') { + return lodashGet(reimbursementAccountDraft, fieldName) + || lodashGet(reimbursementAccount, ['achData', fieldName], defaultValue); } /** @@ -56,5 +43,4 @@ export { clearError, clearErrors, getErrorText, - getBankAccountFields, }; diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 6339f6cfede0..52ca1bd25d39 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -4,6 +4,7 @@ import * as API from '../API'; import ONYXKEYS from '../../ONYXKEYS'; import * as Localize from '../Localize'; import DateUtils from '../DateUtils'; +import * as PlaidDataProps from '../../pages/ReimbursementAccount/plaidDataPropTypes'; export { goToWithdrawalAccountSetupStep, @@ -28,15 +29,16 @@ export { acceptWalletTerms, } from './Wallet'; -function clearPersonalBankAccount() { - Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {}); -} - function clearPlaid() { - Onyx.set(ONYXKEYS.PLAID_DATA, {}); + Onyx.set(ONYXKEYS.PLAID_DATA, PlaidDataProps.plaidDataDefaultProps); Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, ''); } +function clearPersonalBankAccount() { + clearPlaid(); + Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {}); +} + function updatePlaidData(plaidData) { Onyx.merge(ONYXKEYS.PLAID_DATA, plaidData); } diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index 66b593a328da..043556260c7b 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -1,6 +1,5 @@ import _ from 'underscore'; import {createRef} from 'react'; -import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; @@ -10,7 +9,6 @@ import * as Localize from '../Localize'; import Navigation from '../Navigation/Navigation'; import * as CardUtils from '../CardUtils'; import * as User from './User'; -import * as store from './ReimbursementAccount/store'; import ROUTES from '../../ROUTES'; function deletePayPalMe() { @@ -37,19 +35,6 @@ function continueSetup() { kycWallRef.current.continue(); } -/** - * Clears local reimbursement account if it doesn't exist in bankAccounts - * @param {Object[]} bankAccounts - */ -function cleanLocalReimbursementData(bankAccounts) { - const bankAccountID = lodashGet(store.getReimbursementAccountInSetup(), 'bankAccountID'); - - // We check if the bank account list doesn't have the reimbursementAccount - if (!_.find(bankAccounts, bankAccount => bankAccount.bankAccountID === bankAccountID)) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: null, shouldShowResetModal: false}); - } -} - function openPaymentsPage() { const onyxData = { optimisticData: [ @@ -359,7 +344,6 @@ export { resetWalletTransferData, saveWalletTransferAccountTypeAndID, saveWalletTransferMethodType, - cleanLocalReimbursementData, hasPaymentMethodError, clearDeletePaymentMethodError, clearAddPaymentMethodError, diff --git a/src/libs/actions/Plaid.js b/src/libs/actions/Plaid.js index f6691b8f4a3c..fdd8ffc86a04 100644 --- a/src/libs/actions/Plaid.js +++ b/src/libs/actions/Plaid.js @@ -13,7 +13,17 @@ function openPlaidBankLogin(allowDebit, bankAccountID) { const params = getPlaidLinkTokenParameters(); params.allowDebit = allowDebit; params.bankAccountID = bankAccountID; - API.read('OpenPlaidBankLogin', params); + API.read('OpenPlaidBankLogin', params, { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PLAID_DATA, + value: { + isLoading: true, + error: '', + bankAccounts: null, + }, + }], + }); } /** diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js index 40dc5835ab0a..52cba9ccafd7 100644 --- a/src/libs/actions/ReimbursementAccount/index.js +++ b/src/libs/actions/ReimbursementAccount/index.js @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../../ONYXKEYS'; import resetFreePlanBankAccount from './resetFreePlanBankAccount'; import deleteFromBankAccountList from './deleteFromBankAccountList'; +import * as PlaidDataProps from '../../../pages/ReimbursementAccount/plaidDataPropTypes'; export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute} from './navigation'; export { @@ -18,6 +19,11 @@ export { */ function setBankAccountSubStep(subStep) { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {subStep}}); + if (subStep === null) { + Onyx.set(ONYXKEYS.PLAID_DATA, PlaidDataProps.plaidDataDefaultProps); + Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, ''); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, {plaidAccountID: null}); + } } function hideBankAccountErrors() { diff --git a/src/libs/actions/ReimbursementAccount/navigation.js b/src/libs/actions/ReimbursementAccount/navigation.js index f695137764fa..fd6eb0b1d821 100644 --- a/src/libs/actions/ReimbursementAccount/navigation.js +++ b/src/libs/actions/ReimbursementAccount/navigation.js @@ -1,95 +1,19 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import * as store from './store'; -import CONST from '../../../CONST'; import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; import Navigation from '../../Navigation/Navigation'; -const WITHDRAWAL_ACCOUNT_STEPS = [ - { - id: CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, - title: 'Bank Account', - }, - { - id: CONST.BANK_ACCOUNT.STEP.COMPANY, - title: 'Company Information', - }, - { - id: CONST.BANK_ACCOUNT.STEP.REQUESTOR, - title: 'Requestor Information', - }, - { - id: CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT, - title: 'Beneficial Owners', - }, - { - id: CONST.BANK_ACCOUNT.STEP.VALIDATION, - title: 'Validate', - }, - { - id: CONST.BANK_ACCOUNT.STEP.ENABLE, - title: 'Enable', - }, -]; - -/** - * Get step position in the array - * @private - * @param {String} stepID - * @return {Number} - */ -function getIndexByStepID(stepID) { - return _.findIndex(WITHDRAWAL_ACCOUNT_STEPS, step => step.id === stepID); -} - -/** - * Get next step ID - * @param {String} [stepID] - * @return {String} - */ -function getNextStepID(stepID) { - const nextStepIndex = Math.min( - getIndexByStepID(stepID || store.getReimbursementAccountInSetup().currentStep) + 1, - WITHDRAWAL_ACCOUNT_STEPS.length - 1, - ); - return lodashGet(WITHDRAWAL_ACCOUNT_STEPS, [nextStepIndex, 'id'], CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT); -} - -/** - * @param {Object} achData - * @returns {String} - */ -function getNextStepToComplete(achData) { - if (achData.currentStep === CONST.BANK_ACCOUNT.STEP.REQUESTOR && !achData.isOnfidoSetupComplete) { - return CONST.BANK_ACCOUNT.STEP.REQUESTOR; - } - - return getNextStepID(achData.currentStep); -} - /** * Navigate to a specific step in the VBA flow * * @param {String} stepID - * @param {Object} achData + * @param {Object} newAchData */ -function goToWithdrawalAccountSetupStep(stepID, achData) { - const newACHData = {...store.getReimbursementAccountInSetup()}; - - // If we go back to Requestor Step, reset any validation and previously answered questions from expectID. - if (!newACHData.useOnfido && stepID === CONST.BANK_ACCOUNT.STEP.REQUESTOR) { - delete newACHData.questions; - delete newACHData.answers; - } - - // When going back to the BankAccountStep from the Company Step, show the manual form instead of Plaid - if (newACHData.currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && stepID === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT) { - newACHData.subStep = CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL; - } +function goToWithdrawalAccountSetupStep(stepID, newAchData) { + const originalACHData = {...store.getReimbursementAccountInSetup()}; - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {...newACHData, ...achData, currentStep: stepID}}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {...originalACHData, ...newAchData, currentStep: stepID}}); } /** @@ -101,7 +25,5 @@ function navigateToBankAccountRoute() { export { goToWithdrawalAccountSetupStep, - getNextStepToComplete, - getNextStepID, navigateToBankAccountRoute, }; diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js index 24a7919fe824..83d4350fad58 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js @@ -1,13 +1,14 @@ -import lodashGet from 'lodash/get'; import ONYXKEYS from '../../../ONYXKEYS'; import * as store from './store'; import * as API from '../../API'; +import * as PlaidDataProps from '../../../pages/ReimbursementAccount/plaidDataPropTypes'; +import * as ReimbursementAccountProps from '../../../pages/ReimbursementAccount/reimbursementAccountPropTypes'; /** * Reset user's reimbursement account. This will delete the bank account. + * @param {number} bankAccountID */ -function resetFreePlanBankAccount() { - const bankAccountID = lodashGet(store.getReimbursementAccountInSetup(), 'bankAccountID'); +function resetFreePlanBankAccount(bankAccountID) { if (!bankAccountID) { throw new Error('Missing bankAccountID when attempting to reset free plan bank account'); } @@ -30,7 +31,7 @@ function resetFreePlanBankAccount() { { onyxMethod: 'set', key: ONYXKEYS.PLAID_DATA, - value: {}, + value: PlaidDataProps.plaidDataDefaultProps, }, { onyxMethod: 'set', @@ -40,15 +41,12 @@ function resetFreePlanBankAccount() { { onyxMethod: 'set', key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: { - achData: {}, - shouldShowResetModal: false, - }, + value: ReimbursementAccountProps.reimbursementAccountDefaultProps, }, { onyxMethod: 'set', key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, - value: null, + value: {}, }, ], }); diff --git a/src/libs/getPlaidLinkTokenParameters/index.js b/src/libs/getPlaidLinkTokenParameters/index.js index cd15926ee3e6..7998cc3c9447 100644 --- a/src/libs/getPlaidLinkTokenParameters/index.js +++ b/src/libs/getPlaidLinkTokenParameters/index.js @@ -1,7 +1,6 @@ import ROUTES from '../../ROUTES'; -import CONFIG from '../../CONFIG'; export default () => { const bankAccountRoute = window.location.href.includes('personal') ? ROUTES.BANK_ACCOUNT_PERSONAL : ROUTES.BANK_ACCOUNT; - return {redirect_uri: `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${bankAccountRoute}`}; + return {redirect_uri: `https://localhost:8080/${bankAccountRoute}`}; }; diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js index 67be90c921f2..3e009f8e10d6 100644 --- a/src/pages/AddPersonalBankAccountPage.js +++ b/src/pages/AddPersonalBankAccountPage.js @@ -22,10 +22,14 @@ import Button from '../components/Button'; import FixedFooter from '../components/FixedFooter'; import Form from '../components/Form'; import ROUTES from '../ROUTES'; +import * as PlaidDataProps from './ReimbursementAccount/plaidDataPropTypes'; const propTypes = { ...withLocalizePropTypes, + /** Contains plaid data */ + plaidData: PlaidDataProps.plaidDataPropTypes(), + /** The details about the Personal bank account we are adding saved in Onyx */ personalBankAccount: PropTypes.shape({ /** An error message to display to the user */ @@ -43,6 +47,7 @@ const propTypes = { }; const defaultProps = { + plaidData: PlaidDataProps.plaidDataDefaultProps, personalBankAccount: { error: '', shouldShowSuccess: false, @@ -114,7 +119,10 @@ class AddPersonalBankAccountPage extends React.Component {