From c219c9e2abe96fe210e9800de361c6777a8eea24 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 9 Jun 2021 16:28:15 -1000 Subject: [PATCH 01/24] add models --- src/libs/API.js | 43 +++++ src/libs/actions/BankAccounts.js | 217 +++++++++++++++++++++++- src/libs/models/BankAccount.js | 5 + src/pages/AddPersonalBankAccountPage.js | 4 +- 4 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 src/libs/models/BankAccount.js diff --git a/src/libs/API.js b/src/libs/API.js index 5a3918173bbf..e3f73804cb2e 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -812,6 +812,48 @@ function BankAccount_Create(parameters) { return Network.post(commandName, parameters, CONST.NETWORK.METHOD.POST, true); } +/** + * @param {*} parameters + * @returns {Promise} + */ +function BankAccount_SetupWithdrawal(parameters) { + const commandName = 'BankAccount_SetupWithdrawal'; + let allowedParameters = [ + 'currentStep', 'policyID', 'bankAccountID', 'useOnfido', 'errorAttemptsCount', + + // data from bankAccount step: + 'setupType', 'routingNumber', 'accountNumber', 'addressName', 'plaidAccountID', 'ownershipType', 'isSavings', + 'acceptTerms', 'bankName', 'plaidAccessToken', 'alternateRoutingNumber', + + // data from company step: + 'companyName', 'companyTaxID', 'addressStreet', 'addressCity', 'addressState', 'addressZipCode', + 'hasNoConnectionToCannabis', 'incorporationType', 'incorporationState', 'incorporationDate', 'industryCode', + 'website', 'companyPhone', 'ficticiousBusinessName', + + // data from requestor step: + 'firstName', 'lastName', 'dob', 'requestorAddressStreet', 'requestorAddressCity', 'requestorAddressState', + 'requestorAddressZipCode', 'isOnfidoSetupComplete', 'onfidoData', 'isControllingOfficer', 'ssnLast4', + + // data from ACHContract step (which became the "Beneficial Owners" step, but the key is still ACHContract as + // it's used in several logic: + 'ownsMoreThan25Percent', 'beneficialOwners', 'acceptTermsAndConditions', 'certifyTrueInformation', + ]; + + if (!parameters.useOnfido) { + allowedParameters = allowedParameters.concat(['passport', 'answers']); + } + + // Only keep allowed parameters in the additionalData object + const additionalData = _.pick(parameters, allowedParameters); + + requireParameters(['currentStep'], parameters, commandName); + return Network.post( + commandName, {additionalData: JSON.stringify(additionalData), password: parameters.password}, + CONST.NETWORK.METHOD.POST, + true, + ); +} + /** * @param {Object} parameters * @param {String[]} data @@ -850,6 +892,7 @@ export { Authenticate, BankAccount_Create, BankAccount_Get, + BankAccount_SetupWithdrawal, ChangePassword, CreateChatReport, CreateLogin, diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 0b9e2f9e6ee9..e3d3e2851d55 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -5,6 +5,8 @@ import _ from 'underscore'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; +import Navigation from '../Navigation/Navigation'; +import BankAccount from '../models/BankAccount'; /** * Gets the Plaid Link token used to initialize the Plaid SDK @@ -75,7 +77,7 @@ function clearPlaidBankAccounts() { * @param {String} password * @param {String} plaidLinkToken */ -function addPlaidBankAccount(account, password, plaidLinkToken) { +function addPersonalBankAccount(account, password, plaidLinkToken) { const unmaskedAccount = _.find(plaidBankAccounts, bankAccount => ( bankAccount.plaidAccountID === account.plaidAccountID )); @@ -269,12 +271,223 @@ function fetchUserWallet() { }); } +/** + * @private + * @param {String} stepID + * @returns {String} + */ +function getNextWithdrawalAccountSetupStep(stepID) { + const withdrawalAccountSteps = [ + { + id: 'BankAccountStep', + title: 'Bank Account', + }, + { + id: 'CompanyStep', + title: 'Company Information', + }, + { + id: 'RequestorStep', + title: 'Requestor Information', + }, + { + id: 'ACHContractStep', + title: 'Beneficial Owners', + }, + { + id: 'ValidationStep', + title: 'Validate', + }, + { + id: 'EnableStep', + title: 'Enable', + }, + ]; + + const index = _.findIndex(withdrawalAccountSteps, step => step.id === stepID); + const nextStepIndex = Math.min(index + 1, withdrawalAccountSteps.length - 1); + return lodashGet(withdrawalAccountSteps, [nextStepIndex, 'id']); +} + +/** + * @param {String} stepID + * @param {Object} previousACHData + * @param {Object} newACHData + * @returns {Object} + */ +function goToWithdrawlAccountStep(stepID, previousACHData, newACHData = {}) { + // @TODO I'm lost on how to migrate this one... seems like kind of tacked on mutations of the this.achData... + // I would like to maybe get rid of this.achData as a concept here... maybe the achData belongs in Onyx instead + // there's some weird stuff happening like setting the domain limit locally and the logic is not easy to follow yet. + + + const modifiedPreviousACHData = {...previousACHData}; + + // If we go back to Requestor Step, reset any validation and previously answered questions from expectID. + if (!modifiedPreviousACHData.useOnfido && stepID === 'RequestorStep') { + delete modifiedPreviousACHData.questions; + delete modifiedPreviousACHData.answers; + if (_.has(modifiedPreviousACHData, 'verifications.externalApiResponses')) { + delete modifiedPreviousACHData.verifications.externalApiResponses.requestorIdentityID; + delete modifiedPreviousACHData.verifications.externalApiResponses.requestorIdentityKBA; + } + } + + // When going from companyStep to bankAccountStep, show the manual form instead of Plaid + if (modifiedPreviousACHData.currentStep === 'CompanyStep' && stepID === 'BankAccountStep') { + modifiedPreviousACHData.subStep = 'manual'; + } + + _.extend(modifiedPreviousACHData, newACHData, {currentStep: stepID}); + + if (stepID === 'EnableStep') { + // Calculate the Expensify Card limit associated with the bankAccountID + API.BankAccount_CalculateDomainLimit({ + bankAccountID: modifiedPreviousACHData.bankAccountID, + useExistingDomainLimitIfAvailable: true, + }) + .done((response) => { + // @TODO this doesn't really work because we'd have already returned this object - not entirely sure if + // we need to set a domain limit here anyway as the user can't modify it in E.cash + modifiedPreviousACHData.domainLimit = response.domainLimit || 3000000; + }); + } else { + // previously we called refreshView() - but we probably will not do that in the E.cash version and will instead + // call Navigation.navigate() and just go to the view... I think probably we can kill this method also and just + // do this in setupWithdrawalAccount + Navigation.navigate(stepID, modifiedPreviousACHData); + } + + return modifiedPreviousACHData; +} + +/** + * @param {Object} previousACHData + * @param {Object} data + * @returns {Object} + */ +function setupWithdrawalAccount(previousACHData, data) { + // In Web-Secure this is referring to this.achData - still need to look at how that works... + const achData = _.extend(previousACHData, { + ...data, + isSavings: data + && !_.isUndefined(data.isSavings) + && Boolean(data.isSavings), + }); + + if (!achData.setupType) { + // @TODO use CONST + achData.setupType = achData.plaidAccountID ? 'plaid' : 'manual'; + } + + let nextStep = achData.currentStep; + + API.BankAccount_SetupWithdrawal(achData) + .finally((response) => { + const currentStep = achData.currentStep; + let responseACHData = response.achData; + let error = lodashGet(responseACHData, 'verifications.errorMessage'); + + if (response.jsonCode === 200 && !error) { + // Show warning if another account already set up this bank account and promote share + if (response.existingOwners) { + // @TODO show this error + return; + } + + // @TODO use CONST for step name + if (currentStep === 'RequestorStep') { + // @TODO use CONST + const requestorResponse = lodashGet( + responseACHData, + 'verifications.externalApiResponses.requestorIdentityID', + ); + + if (responseACHData.useOnFido) { + const onfidoResponse = lodashGet( + responseACHData, 'verifications.externalApiResponses.requstorIdentityOnfido', + ); + const sdkToken = lodashGet(onfidoResponse, 'apiResult.sdkToken'); + if (sdkToken && !achData.isOnfidoSetupComplete && onfidoResponse.status !== 'pass') { + // Requestor Step still needs to run Onfido + responseACHData.sdkToken = sdkToken; + + // Navigate to "RequestorStep" with responseACHData + Navigation.navigate('/bank-account/requestor', responseACHData); + return; + } + } else if (requestorResponse) { + // Don't go to next step if Requestor Step needs to ask some questions + let questions = _.get(requestorResponse, 'apiResult.questions.question') || []; + if (_.isEmpty(questions)) { + const differentiatorQuestion = lodashGet( + requestorResponse, + 'apiResult.differentiator-question', + ); + if (differentiatorQuestion) { + questions = [differentiatorQuestion]; + } + } + if (!_.isEmpty(questions)) { + responseACHData.questions = questions; + Navigation.navigate('/bank-account/requestor', responseACHData); + return; + } + } + } + + // @TODO figure out why this is returning a promise... + if (currentStep === 'ACHContractStep') { + // Get an up-to-date bank account list so that we can allow the user to validate their newly + // generated bank account + return API.Get({returnValueList: 'bankAccountList'}, true) + .then((json) => { + const bankAccount = new BankAccount( + _.findWhere(json.bankAccountList, {bankAccountID: achData.bankAccountID}), + ); + responseACHData = bankAccount.toACHData(); + const needsToPassLatestChecks = responseACHData.state === BankAccount.STATE.OPEN + && responseACHData.needsToPassLatestChecks; + responseACHData.bankAccountInReview = needsToPassLatestChecks + || responseACHData.state === BankAccount.STATE.VERIFYING; + Navigation.navigate('/bank-account/validation', responseACHData); + }); + } + + if ((currentStep === 'ValidationStep' && achData.bankAccountInReview) + || currentStep === 'EnableStep' + ) { + // We're done. Close the view. + // @TODO actually close it + } else { + nextStep = getNextWithdrawalAccountSetupStep(achData); + } + } else { + if (response.jsonCode === 666) { + error = response.message; + } + if (lodashGet(responseACHData, 'verifications.throttled')) { + responseACHData.disableFields = true; + } + } + + goToWithdrawlAccountStep(nextStep, achData, responseACHData); + + if (error) { + // @TODO Show the error somehow + } + }); + + return achData; +} + export { fetchPlaidLinkToken, - addPlaidBankAccount, + addPersonalBankAccount, getPlaidBankAccounts, clearPlaidBankAccounts, fetchOnfidoToken, activateWallet, fetchUserWallet, + setupWithdrawalAccount, }; diff --git a/src/libs/models/BankAccount.js b/src/libs/models/BankAccount.js new file mode 100644 index 000000000000..02566acabbbd --- /dev/null +++ b/src/libs/models/BankAccount.js @@ -0,0 +1,5 @@ +class BankAccount { + +} + +export default BankAccount; diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js index 9fb582421717..787773244b47 100644 --- a/src/pages/AddPersonalBankAccountPage.js +++ b/src/pages/AddPersonalBankAccountPage.js @@ -14,7 +14,7 @@ import ScreenWrapper from '../components/ScreenWrapper'; import Navigation from '../libs/Navigation/Navigation'; import PlaidLink from '../components/PlaidLink'; import { - addPlaidBankAccount, + addPersonalBankAccount, clearPlaidBankAccounts, fetchPlaidLinkToken, getPlaidBankAccounts, @@ -79,7 +79,7 @@ class AddPersonalBankAccountPage extends React.Component { addSelectedAccount() { const account = this.getAccounts()[this.state.selectedIndex]; - addPlaidBankAccount(account, this.state.password, this.props.plaidLinkToken); + addPersonalBankAccount(account, this.state.password, this.props.plaidLinkToken); this.setState({isCreatingAccount: true}); } From 6eac7d48d5c8e11ea73a95dfdc0605c0c2677d19 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 10 Jun 2021 09:47:30 -1000 Subject: [PATCH 02/24] add BankAccount model --- src/CONST.js | 6 + src/libs/models/BankAccount.js | 322 +++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+) diff --git a/src/CONST.js b/src/CONST.js index 71a80cf4f0b7..09cb88d02b9f 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -19,6 +19,12 @@ const CONST = { PRESSED: 'pressed', COMPLETE: 'complete', }, + COUNTRY: { + US: 'US', + MX: 'MX', + AU: 'AU', + CA: 'CA', + }, PLATFORM: { IOS: 'ios', ANDROID: 'android', diff --git a/src/libs/models/BankAccount.js b/src/libs/models/BankAccount.js index 02566acabbbd..cfecacb67af6 100644 --- a/src/libs/models/BankAccount.js +++ b/src/libs/models/BankAccount.js @@ -1,5 +1,327 @@ +import _ from 'underscore'; +import Onyx from 'react-native-onyx'; +import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; +import lodashHas from 'lodash/has'; +import CONST from '../../CONST'; +import ONYXKEYS from '../../ONYXKEYS'; + +let currentUserLogin; + +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: val => currentUserLogin = val && val.email, +}); + class BankAccount { + static STATE = { + PENDING: 'PENDING', + OPEN: 'OPEN', + DELETED: 'DELETED', + LOCKED: 'LOCKED', + SETUP: 'SETUP', + VERIFYING: 'VERIFYING', + }; + + constructor(accountJSON) { + this.json = accountJSON; + } + + /** + * Return the ID of the reimbursement account + * + * @returns {Number} + */ + getID() { + return this.json.bankAccountID; + } + + /** + * Return the account number, which has been obfuscate by the back end + * example "XXXXXX3956" + * + * @returns {String} + */ + getMaskedAccountNumber() { + return this.json.accountNumber; + } + + /** + * Used as the display name for the account... + * @returns {String} + */ + getAddressName() { + return this.json.addressName; + } + + /** + * @returns {String} + */ + getProcessor() { + return this.json.processor; + } + + /** + * @returns {String} + */ + getRoutingNumber() { + return this.json.routingNumber; + } + + /** + * Get all user emails having access to this bank account + * @return {String[]} + */ + getSharees() { + return this.json.sharees; + } + + /** + * @returns {String} + * @private + */ + getState() { + return this.json.state; + } + + /** + * @returns {Boolean} + */ + isOpen() { + return this.getState() === BankAccount.STATE.OPEN; + } + + /** + * @deprecated Use !isPending instead. + * @returns {Boolean} + */ + isVerified() { + return !this.isPending(); + } + + /** + * If the user still needs to enter the 3 micro deposit amounts. + * @returns {Boolean} + */ + isPending() { + return this.getState() === BankAccount.STATE.PENDING; + } + + /** + * If success team is currently verifying the bank account data provided by the user. + * @returns {Boolean} + */ + isVerifying() { + return Boolean(this.json.validating) || this.getState() === BankAccount.STATE.VERIFYING; + } + + /** + * If the user didn't finish entering all his info. + * @returns {Boolean} + */ + isInSetup() { + return this.getState() === BankAccount.STATE.SETUP; + } + + /** + * @returns {Boolean} + */ + isLocked() { + return this.getState() === BankAccount.STATE.LOCKED; + } + + /** + * If someone asked to share the bank account with me, and the request is still pending + * @returns {Boolean} + */ + isSharePending() { + return Boolean(this.json.shareComplete === false && this.getOwner() !== currentUserLogin); + } + + /** + * Who shared this account with me? + * @returns {String} + */ + getOwner() { + return this.json.ownedBy; + } + + /** + * Is it the account to use by default to receive money? + * + * @returns {Boolean} + */ + isDefaultCredit() { + return this.json.defaultCredit === true; + } + + /** + * Can we use this account to pay other people? + * + * @returns {Boolean} + */ + isWithdrawal() { + return this.json.allowDebit === true; + } + + /** + * Get when the user last updated their bank account. + * @return {*|String} + */ + getDateSigned() { + return lodashGet(this.json, ['additionalData', 'dateSigned']) || ''; + } + + /** + * Return the client ID of this bank account + * + * @NOTE WARNING KEEP IN SYNC WITH THE PHP + * @returns {String} + */ + getClientID() { + // eslint-disable-next-line max-len + return `${Str.makeID(this.getMaskedAccountNumber())}${Str.makeID(this.getAddressName())}${Str.makeID(this.getRoutingNumber())}${this.getType()}`; + } + + /** + * @returns {String} + * @private + */ + getType() { + return this.isWithdrawal() ? 'withdrawal' : 'direct-deposit'; + } + + /** + * Return the internal json data structure used by auth + * @returns {Object} + */ + getJSON() { + return this.json; + } + + /** + * Return whether or not this bank account has been risk checked + * @returns {Boolean} + */ + isRiskChecked() { + return Boolean(this.json.riskChecked); + } + + /** + * Return when the 3 micro amounts for validation were supposed to reach the bank account. + * @returns {String} + */ + getValidateCodeExpectedDate() { + return this.json.validateCodeExpectedDate || ''; + } + + /** + * In which country is the bank account? + * @returns {string} + */ + getCountry() { + return lodashGet(this.json, ['additionalData', 'country'], CONST.COUNTRY.US); + } + + /** + * In which currency is the bank account? + * @returns {String} + */ + getCurrency() { + return lodashGet(this.json, ['additionalData', 'currency'], 'USD'); + } + + /** + * In which bank is the bank account? + * @returns {String} + */ + getBankName() { + return lodashGet(this.json, ['additionalData', 'bankName'], lodashGet(this.json, 'bankName')); + } + + /** + * Did we get bank account details for local transfer or international wire? + * @returns {Boolean} + */ + hasInternationalWireDetails() { + return lodashGet(this.json, ['additionalData', 'fieldsType'], 'local') === 'international'; + } + + /** + * Get the additional data of a bankAccount + * @returns {Object} + */ + getAdditionalData() { + return this.json.additionalData || {}; + } + + /** + * Return a map needed to setup a withdrawal account + * @returns {Object} + */ + toACHData() { + return _.extend({ + routingNumber: this.getRoutingNumber(), + accountNumber: this.getMaskedAccountNumber(), + addressName: this.getAddressName(), + isSavings: this.json.isSavings, + bankAccountID: this.getID(), + state: this.getState(), + validateCodeExpectedDate: this.getValidateCodeExpectedDate(), + needsToPassLatestChecks: this.needsToPassLatestChecks(), + needsToUpgrade: this.needsToUpgrade(), + }, this.getAdditionalData()); + } + + + /** + * Check if user hasn't upgraded their bank account yet. + * @return {Boolean} + */ + needsToUpgrade() { + return !this.isInSetup() && !lodashHas(this.json, ['additionalData', 'beneficialOwners']); + } + + /** + * Check if we've performed the most recently implemented checks on the bank account, and they all passed. + * Same logic as in BankAccount.php needsToPassLatestChecks + * @return {Boolean} + */ + needsToPassLatestChecks() { + if (!lodashGet(this.json, ['additionalData', 'hasFullSSN'])) { + return true; + } + + const beneficialOwners = lodashGet(this.json, ['additionalData', 'beneficialOwners']); + if (!beneficialOwners) { + return true; + } + + const city = lodashGet(this.json, ['additionalData', 'requestorAddressCity']); + if (!city) { + return true; + } + + if (_.isArray(beneficialOwners)) { + const hasBeneficialOwnerError = _.any(beneficialOwners, (beneficialOwner) => { + const hasFullSSN = lodashGet(beneficialOwner, 'hasFullSSN') + || !_.isEmpty(lodashGet(beneficialOwner, 'ssn')); + return !lodashGet(beneficialOwner, 'isRequestor') + && (lodashGet(beneficialOwner, ['expectIDPA', 'status']) !== 'pass' + || !lodashGet(beneficialOwner, 'city') || !hasFullSSN + ); + }); + if (hasBeneficialOwnerError) { + return true; + } + } + return _.any(['realSearchResult', 'lexisNexisInstantIDResult', 'requestorIdentityID'], field => ( + lodashGet(this.json, [ + 'additionalData', 'verifications', 'externalApiResponses', field, 'status', + ]) !== 'pass' + )); + } } export default BankAccount; From cc78e0169af567d8e6bf9ea10671649c995403d9 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 10 Jun 2021 10:15:45 -1000 Subject: [PATCH 03/24] update some consts --- src/CONST.js | 32 +++++++++++++ src/libs/actions/BankAccounts.js | 78 ++++++++++++++++++-------------- 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 09cb88d02b9f..edaee01ab906 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -6,6 +6,38 @@ const CONST = { IOS: 'https://apps.apple.com/us/app/expensify-cash/id1530278510', DESKTOP: 'https://expensify.cash/Expensify.cash.dmg', }, + BANK_ACCOUNT: { + STEP: { + BANK_ACCOUNT: 'BankAccountStep', + COMPANY: 'CompanyStep', + REQUESTOR: 'RequestorStep', + ACH_CONTRACT: 'ACHContractStep', + VALIDATION: 'ValidationStep', + ENABLE: 'EnableStep', + }, + SUBSTEP: { + MANUAL: 'manual', + }, + VERIFICATIONS: { + ERROR_MESSAGE: 'verifications.errorMessage', + EXTERNAL_API_RESPONSES: 'verifications.externalApiResponses', + REQUESTOR_IDENTITY_ID: 'verifications.externalApiResponses.requestorIdentityID', + REQUESTOR_IDENTITY_ONFIDO: 'verifications.externalApiResponses.requestorIdentityOnfido', + THROTTLED: 'verifications.throttled', + }, + ONFIDO_RESPONSE: { + SDK_TOKEN: 'apiResult.sdkToken', + PASS: 'pass', + }, + QUESTIONS: { + QUESTION: 'apiResult.questions.question', + DIFFERENTIATOR_QUESTION: 'apiResult.differentiator-question', + }, + SETUP_TYPE: { + MANUAL: 'manual', + PLAID: 'plaid', + }, + }, BETAS: { ALL: 'all', CHRONOS_IN_CASH: 'chronosInCash', diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index e3d3e2851d55..3217bdc1375f 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -1,4 +1,5 @@ import lodashGet from 'lodash/get'; +import lodashHas from 'lodash/has'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import _ from 'underscore'; @@ -279,27 +280,27 @@ function fetchUserWallet() { function getNextWithdrawalAccountSetupStep(stepID) { const withdrawalAccountSteps = [ { - id: 'BankAccountStep', + id: CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, title: 'Bank Account', }, { - id: 'CompanyStep', + id: CONST.BANK_ACCOUNT.STEP.COMPANY, title: 'Company Information', }, { - id: 'RequestorStep', + id: CONST.BANK_ACCOUNT.STEP.REQUESTOR, title: 'Requestor Information', }, { - id: 'ACHContractStep', + id: CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT, title: 'Beneficial Owners', }, { - id: 'ValidationStep', + id: CONST.BANK_ACCOUNT.STEP.VALIDATION, title: 'Validate', }, { - id: 'EnableStep', + id: CONST.BANK_ACCOUNT.STEP.ENABLE, title: 'Enable', }, ]; @@ -320,37 +321,40 @@ function goToWithdrawlAccountStep(stepID, previousACHData, newACHData = {}) { // I would like to maybe get rid of this.achData as a concept here... maybe the achData belongs in Onyx instead // there's some weird stuff happening like setting the domain limit locally and the logic is not easy to follow yet. - const modifiedPreviousACHData = {...previousACHData}; // If we go back to Requestor Step, reset any validation and previously answered questions from expectID. - if (!modifiedPreviousACHData.useOnfido && stepID === 'RequestorStep') { + if (!modifiedPreviousACHData.useOnfido && stepID === CONST.BANK_ACCOUNT.STEP.REQUESTOR) { delete modifiedPreviousACHData.questions; delete modifiedPreviousACHData.answers; - if (_.has(modifiedPreviousACHData, 'verifications.externalApiResponses')) { + if (lodashHas(modifiedPreviousACHData, CONST.BANK_ACCOUNT.VERIFICATIONS.EXTERNAL_API_RESPONSES)) { delete modifiedPreviousACHData.verifications.externalApiResponses.requestorIdentityID; delete modifiedPreviousACHData.verifications.externalApiResponses.requestorIdentityKBA; } } // When going from companyStep to bankAccountStep, show the manual form instead of Plaid - if (modifiedPreviousACHData.currentStep === 'CompanyStep' && stepID === 'BankAccountStep') { - modifiedPreviousACHData.subStep = 'manual'; + if (modifiedPreviousACHData.currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY + && stepID === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT + ) { + modifiedPreviousACHData.subStep = CONST.BANK_ACCOUNT.SUBSTEP.MANUAL; } _.extend(modifiedPreviousACHData, newACHData, {currentStep: stepID}); - if (stepID === 'EnableStep') { + if (stepID === CONST.BANK_ACCOUNT.STEP.ENABLE) { + console.debug('EnableStep...'); + + // @TODO - Pretty sure we do not need to do this step here... + // as 1. is not a write command 2. we are getting rid of the EnableStep entirely as per the doc... // Calculate the Expensify Card limit associated with the bankAccountID - API.BankAccount_CalculateDomainLimit({ - bankAccountID: modifiedPreviousACHData.bankAccountID, - useExistingDomainLimitIfAvailable: true, - }) - .done((response) => { - // @TODO this doesn't really work because we'd have already returned this object - not entirely sure if - // we need to set a domain limit here anyway as the user can't modify it in E.cash - modifiedPreviousACHData.domainLimit = response.domainLimit || 3000000; - }); + // API.BankAccount_CalculateDomainLimit({ + // bankAccountID: modifiedPreviousACHData.bankAccountID, + // useExistingDomainLimitIfAvailable: true, + // }) + // .done((response) => { + // modifiedPreviousACHData.domainLimit = response.domainLimit || 3000000; + // }); } else { // previously we called refreshView() - but we probably will not do that in the E.cash version and will instead // call Navigation.navigate() and just go to the view... I think probably we can kill this method also and just @@ -376,8 +380,9 @@ function setupWithdrawalAccount(previousACHData, data) { }); if (!achData.setupType) { - // @TODO use CONST - achData.setupType = achData.plaidAccountID ? 'plaid' : 'manual'; + achData.setupType = achData.plaidAccountID + ? CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID + : CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL; } let nextStep = achData.currentStep; @@ -386,7 +391,7 @@ function setupWithdrawalAccount(previousACHData, data) { .finally((response) => { const currentStep = achData.currentStep; let responseACHData = response.achData; - let error = lodashGet(responseACHData, 'verifications.errorMessage'); + let error = lodashGet(responseACHData, CONST.BANK_ACCOUNT.VERIFICATIONS.ERROR_MESSAGE); if (response.jsonCode === 200 && !error) { // Show warning if another account already set up this bank account and promote share @@ -396,19 +401,22 @@ function setupWithdrawalAccount(previousACHData, data) { } // @TODO use CONST for step name - if (currentStep === 'RequestorStep') { + if (currentStep === CONST.BANK_ACCOUNT.STEP.REQUESTOR) { // @TODO use CONST const requestorResponse = lodashGet( responseACHData, - 'verifications.externalApiResponses.requestorIdentityID', + CONST.BANK_ACCOUNT.VERIFICATIONS.REQUESTOR_IDENTITY_ID, ); if (responseACHData.useOnFido) { const onfidoResponse = lodashGet( - responseACHData, 'verifications.externalApiResponses.requstorIdentityOnfido', + responseACHData, CONST.BANK_ACCOUNT.VERIFICATIONS.REQUESTOR_IDENTITY_ONFIDO, ); - const sdkToken = lodashGet(onfidoResponse, 'apiResult.sdkToken'); - if (sdkToken && !achData.isOnfidoSetupComplete && onfidoResponse.status !== 'pass') { + const sdkToken = lodashGet(onfidoResponse, CONST.BANK_ACCOUNT.ONFIDO_RESPONSE.SDK_TOKEN); + if (sdkToken + && !achData.isOnfidoSetupComplete + && onfidoResponse.status !== CONST.BANK_ACCOUNT.ONFIDO_RESPONSE.PASS + ) { // Requestor Step still needs to run Onfido responseACHData.sdkToken = sdkToken; @@ -418,11 +426,11 @@ function setupWithdrawalAccount(previousACHData, data) { } } else if (requestorResponse) { // Don't go to next step if Requestor Step needs to ask some questions - let questions = _.get(requestorResponse, 'apiResult.questions.question') || []; + let questions = _.get(requestorResponse, CONST.BANK_ACCOUNT.QUESTIONS.QUESTION) || []; if (_.isEmpty(questions)) { const differentiatorQuestion = lodashGet( requestorResponse, - 'apiResult.differentiator-question', + CONST.BANK_ACCOUNT.QUESTIONS.DIFFERENTIATOR_QUESTION, ); if (differentiatorQuestion) { questions = [differentiatorQuestion]; @@ -437,7 +445,7 @@ function setupWithdrawalAccount(previousACHData, data) { } // @TODO figure out why this is returning a promise... - if (currentStep === 'ACHContractStep') { + if (currentStep === CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT) { // Get an up-to-date bank account list so that we can allow the user to validate their newly // generated bank account return API.Get({returnValueList: 'bankAccountList'}, true) @@ -454,8 +462,8 @@ function setupWithdrawalAccount(previousACHData, data) { }); } - if ((currentStep === 'ValidationStep' && achData.bankAccountInReview) - || currentStep === 'EnableStep' + if ((currentStep === CONST.BANK_ACCOUNT.STEP.VALIDATION && achData.bankAccountInReview) + || currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE ) { // We're done. Close the view. // @TODO actually close it @@ -466,7 +474,7 @@ function setupWithdrawalAccount(previousACHData, data) { if (response.jsonCode === 666) { error = response.message; } - if (lodashGet(responseACHData, 'verifications.throttled')) { + if (lodashGet(responseACHData, CONST.BANK_ACCOUNT.VERIFICATIONS.THROTTLED)) { responseACHData.disableFields = true; } } From c810c56a477b84d38ecaa3e7cb992904f79bc187 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 10 Jun 2021 12:41:46 -1000 Subject: [PATCH 04/24] attempt to migrate stuff to a controller page for now while make sense of it all --- .../Navigation/AppNavigator/AuthScreens.js | 7 + .../AppNavigator/ModalStackNavigators.js | 7 + src/libs/Navigation/linkingConfig.js | 5 + src/libs/actions/BankAccounts.js | 2 + src/pages/ReimbursementAccountPage.js | 130 ++++++++++++++++++ 5 files changed, 151 insertions(+) create mode 100644 src/pages/ReimbursementAccountPage.js diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index a032193f97f8..f46b73793b3d 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -50,6 +50,7 @@ import { SettingsModalStackNavigator, EnablePaymentsStackNavigator, AddPersonalBankAccountModalStackNavigator, + ReimbursementAccountModalStackNavigator, } from './ModalStackNavigators'; import SCREENS from '../../../SCREENS'; import Timers from '../../Timers'; @@ -275,6 +276,12 @@ class AuthScreens extends React.Component { component={AddPersonalBankAccountModalStackNavigator} listeners={modalScreenListeners} /> + ); } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 7a3685f25067..91c7fd9a5aae 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -21,6 +21,7 @@ import IOUCurrencySelection from '../../../pages/iou/IOUCurrencySelection'; import ReportParticipantsPage from '../../../pages/ReportParticipantsPage'; import EnablePaymentsPage from '../../../pages/EnablePayments'; import AddPersonalBankAccountPage from '../../../pages/AddPersonalBankAccountPage'; +import ReimbursementAccountPage from '../../../pages/ReimbursementAccountPage'; const defaultSubRouteOptions = { cardStyle: styles.navigationScreenCardStyle, @@ -152,6 +153,11 @@ const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator([{ name: 'AddPersonalBankAccount_Root', }]); +const ReimbursementAccountModalStackNavigator = createModalStackNavigator([{ + Component: ReimbursementAccountPage, + name: 'ReimbursementAccount_Root', +}]); + export { IOUBillStackNavigator, IOURequestModalStackNavigator, @@ -164,4 +170,5 @@ export { SettingsModalStackNavigator, EnablePaymentsStackNavigator, AddPersonalBankAccountModalStackNavigator, + ReimbursementAccountModalStackNavigator, }; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 7053c8d4f021..f3652f951d34 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -114,6 +114,11 @@ export default { EnablePayments_Root: ROUTES.ENABLE_PAYMENTS, }, }, + ReimbursementAccount: { + screens: { + ReimbursementAccount_Root: 'reimbursement-account', + }, + }, }, }, }; diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 3217bdc1375f..3a8a4eea2f6c 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -299,6 +299,8 @@ function getNextWithdrawalAccountSetupStep(stepID) { id: CONST.BANK_ACCOUNT.STEP.VALIDATION, title: 'Validate', }, + + // @TODO maybe can be removed depending on how we handle the enable step moving forward { id: CONST.BANK_ACCOUNT.STEP.ENABLE, title: 'Enable', diff --git a/src/pages/ReimbursementAccountPage.js b/src/pages/ReimbursementAccountPage.js new file mode 100644 index 000000000000..d09a091d1332 --- /dev/null +++ b/src/pages/ReimbursementAccountPage.js @@ -0,0 +1,130 @@ +/* eslint-disable max-len */ +/* eslint-disable react/no-unused-state */ +import lodashGet from 'lodash/get'; +import React from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import ScreenWrapper from '../components/ScreenWrapper'; + +const WITHDRAWAL_ACCOUNT_STEPS = [ + { + id: 'BankAccountStep', + title: 'Bank Account', + }, + { + id: 'CompanyStep', + title: 'Company Information', + }, + { + id: 'RequestorStep', + title: 'Requestor Information', + }, + { + id: 'ACHContractStep', + title: 'Beneficial Owners', + }, + { + id: 'ValidationStep', + title: 'Validate', + }, + { + id: 'EnableStep', + title: 'Enable', + }, +]; + +const propTypes = { + skipOnfido: PropTypes.bool, +}; + +const defaultProps = { + skipOnfido: false, +}; + +class ReimbursementAccountPage extends React.Component { + constructor(props) { + super(props); + + this.state = { + bankAccountID: undefined, + isLoading: true, + isPlaidDisabled: false, + achData: {}, + isWithdrawal: true, + lastDataWithError: {}, + currentStep: 'BankAccountStep', + }; + } + + componentDidMount() { + // If the user already is already setting up a bank account we will want to continue the flow for them + let currentStep; + const bankAccountID = parseInt(this.props.bankAccountID, 10) || 0; // This will need to come from either NVP or route param + const bankAccount = bankAccountID ? this.props.bankAccounts[bankAccountID] : null; // The list of bankAccounts will need to come from Onyx + const achData = bankAccount ? bankAccount.toACHData() : {}; + achData.useOnfido = !this.props.skipOnfido; + achData.policyID = this.props.policyId || ''; + achData.plaidLinkToken = this.props.plaidLinkToken; // I think probably we won't have this until we start the Plaid flow but maybe...? + achData.isInSetup = !bankAccount || bankAccount.isInSetup(); + achData.bankAccountInReview = bankAccount && bankAccount.isVerifying(); + achData.domainLimi = 0; + achData.isDomainUsingExpensifyCard = false; // Maybe also needs to be a prop... not too sure. + achData.subStep = this.props.subStep; // Unsure what the substeps are used for so far... + + // If we're not in setup, it means we already have a withdrawal account and we're upgrading it to a business bank account. + // So let the user review all steps with all info prefilled and editable, unless a specific step was passed. + if (!achData.isInSetup) { + currentStep = this.props.currentStep; // Not sure if we need this as user's won't be upgrading these accounts... + } + + // Temporary fix for Onfido flow. Can be removed by nkuoch after Sept 1 2020. - not sure if we need this or what this is about... + if (currentStep === 'ACHContractStep' && achData.useOnfido) { + const onfidoRes = lodashGet(achData, 'verifications.externalApiResponses.requestorIdentityOnfido'); + const sdkToken = lodashGet(onfidoRes, 'apiResult.sdkToken'); + if (sdkToken && !achData.isOnfidoSetupComplete && onfidoRes.status !== 'pass') { + currentStep = 'RequestorStep'; + } + } + + // Ensure we route the user to the correct step based on the status of their bank account + if (bankAccount && !currentStep) { + currentStep = bankAccount.isPending() || bankAccount.isVerifying() ? 'ValidationStep' : 'BankAccountStep'; + + // Again, not sure how much of this logic is needed right now as we shouldn't be handling any open accounts in E.cash yet... + if (bankAccount.isOpen()) { + if (bankAccount.needsToPassLatestChecks()) { + // const hasTriedToUpgrade = bankAccount.getDateSigned() > (NVP.get('expensify_migration_2020_04_28_RunKycVerifications') || '2020-01-13'); + // currentStep = hasTriedToUpgrade ? 'ValidationStep' : 'CompanyStep'; + // achData.bankAccountInReview = hasTriedToUpgrade; + } else { + // Not handling the EnableStep... + // currentStep = 'EnableStep'; + } + } + } + + // If at this point we still don't have a current step, default to the BankAccountStep + if (!currentStep) { + currentStep = 'BankAccountStep'; + } + + console.log({currentStep, achData}); + this.setState({isLoading: false, currentStep, achData}); + } + + render() { + if (this.state.isLoading) { + return null; + } + + return ( + + + + ); + } +} + +ReimbursementAccountPage.propTypes = propTypes; +ReimbursementAccountPage.defaultProps = defaultProps; +export default ReimbursementAccountPage; From 4f96379f03ec190148f3ba71cb8215f007f2722f Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 10 Jun 2021 18:27:31 -1000 Subject: [PATCH 05/24] sketch out plan --- src/CONST.js | 2 + src/ONYXKEYS.js | 4 + .../Navigation/AppNavigator/AuthScreens.js | 1 + src/libs/actions/BankAccounts.js | 246 ++---------- src/pages/ReimbursementAccountPage.js | 356 +++++++++++++++++- 5 files changed, 389 insertions(+), 220 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index edaee01ab906..5cc8bd26354b 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -147,6 +147,8 @@ const CONST = { PAYPAL_ME_ADDRESS: 'expensify_payPalMeAddress', PRIORITY_MODE: 'priorityMode', TIMEZONE: 'timeZone', + FREE_PLAN_BANK_ACCOUNT_ID: 'expensify_freePlanBankAccountID', + ACH_DATA_THROTTLED: 'expensify_ACHData_throttled', }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {error: '', success: '', loading: false}, diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index d7fcd65074f3..b3271f8b3911 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -75,6 +75,7 @@ export default { // Collection Keys COLLECTION: { + BANK_ACCOUNT: 'bankAccount_', REPORT: 'report_', REPORT_ACTIONS: 'reportActions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', @@ -98,4 +99,7 @@ export default { // Object containing Wallet terms step state WALLET_TERMS: 'walletTerms', + + // Stores information about the free plan bank account being set up + FREE_PLAN_BANK_ACCOUNT: 'freePlanBankAccount', }; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index f46b73793b3d..7aaf98cc72cd 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -13,6 +13,7 @@ import { fetchAllReports, } from '../../actions/Report'; import * as PersonalDetails from '../../actions/PersonalDetails'; +import * as BankAccounts from '../../actions/BankAccounts'; import * as Pusher from '../../Pusher/pusher'; import PusherConnectionManager from '../../PusherConnectionManager'; import UnreadIndicatorUpdater from '../../UnreadIndicatorUpdater'; diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 3a8a4eea2f6c..33c3f72f7c86 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -1,13 +1,10 @@ import lodashGet from 'lodash/get'; -import lodashHas from 'lodash/has'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import _ from 'underscore'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; -import Navigation from '../Navigation/Navigation'; -import BankAccount from '../models/BankAccount'; /** * Gets the Plaid Link token used to initialize the Plaid SDK @@ -273,222 +270,45 @@ function fetchUserWallet() { } /** - * @private - * @param {String} stepID - * @returns {String} - */ -function getNextWithdrawalAccountSetupStep(stepID) { - const withdrawalAccountSteps = [ - { - 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', - }, - - // @TODO maybe can be removed depending on how we handle the enable step moving forward - { - id: CONST.BANK_ACCOUNT.STEP.ENABLE, - title: 'Enable', - }, - ]; - - const index = _.findIndex(withdrawalAccountSteps, step => step.id === stepID); - const nextStepIndex = Math.min(index + 1, withdrawalAccountSteps.length - 1); - return lodashGet(withdrawalAccountSteps, [nextStepIndex, 'id']); -} - -/** - * @param {String} stepID - * @param {Object} previousACHData - * @param {Object} newACHData - * @returns {Object} + * Fetch the bank account currently being set up by the user for the free plan if it exists. */ -function goToWithdrawlAccountStep(stepID, previousACHData, newACHData = {}) { - // @TODO I'm lost on how to migrate this one... seems like kind of tacked on mutations of the this.achData... - // I would like to maybe get rid of this.achData as a concept here... maybe the achData belongs in Onyx instead - // there's some weird stuff happening like setting the domain limit locally and the logic is not easy to follow yet. - - const modifiedPreviousACHData = {...previousACHData}; - - // If we go back to Requestor Step, reset any validation and previously answered questions from expectID. - if (!modifiedPreviousACHData.useOnfido && stepID === CONST.BANK_ACCOUNT.STEP.REQUESTOR) { - delete modifiedPreviousACHData.questions; - delete modifiedPreviousACHData.answers; - if (lodashHas(modifiedPreviousACHData, CONST.BANK_ACCOUNT.VERIFICATIONS.EXTERNAL_API_RESPONSES)) { - delete modifiedPreviousACHData.verifications.externalApiResponses.requestorIdentityID; - delete modifiedPreviousACHData.verifications.externalApiResponses.requestorIdentityKBA; - } - } - - // When going from companyStep to bankAccountStep, show the manual form instead of Plaid - if (modifiedPreviousACHData.currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY - && stepID === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT - ) { - modifiedPreviousACHData.subStep = CONST.BANK_ACCOUNT.SUBSTEP.MANUAL; - } - - _.extend(modifiedPreviousACHData, newACHData, {currentStep: stepID}); - - if (stepID === CONST.BANK_ACCOUNT.STEP.ENABLE) { - console.debug('EnableStep...'); +function fetchFreePlanVerifiedBankAccount() { + Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: true}); + let bankAccountID; + let bankAccount; + API.Get({ + returnValueList: 'nameValuePairs', + name: CONST.NVP.FREE_PLAN_BANK_ACCOUNT_ID, + }) + .then((response) => { + bankAccountID = lodashGet(response.nameValuePairs, CONST.NVP.FREE_PLAN_BANK_ACCOUNT_ID, ''); - // @TODO - Pretty sure we do not need to do this step here... - // as 1. is not a write command 2. we are getting rid of the EnableStep entirely as per the doc... - // Calculate the Expensify Card limit associated with the bankAccountID - // API.BankAccount_CalculateDomainLimit({ - // bankAccountID: modifiedPreviousACHData.bankAccountID, - // useExistingDomainLimitIfAvailable: true, - // }) - // .done((response) => { - // modifiedPreviousACHData.domainLimit = response.domainLimit || 3000000; - // }); - } else { - // previously we called refreshView() - but we probably will not do that in the E.cash version and will instead - // call Navigation.navigate() and just go to the view... I think probably we can kill this method also and just - // do this in setupWithdrawalAccount - Navigation.navigate(stepID, modifiedPreviousACHData); - } + if (!bankAccountID) { + return Promise.resolve({}); + } - return modifiedPreviousACHData; + return API.Get({returnValueList: 'bankAccountList'}); + }) + .then((response) => { + bankAccount = _.find(response.bankAccountList, account => account.bankAccountID === bankAccountID); + return API.Get({ + returnValueList: 'nameValuePairs', + name: CONST.NVP.ACH_DATA_THROTTLED, + }); + }) + .then((response) => { + const throttledDate = lodashGet(response.nameValuePairs, CONST.NVP.ACH_DATA_THROTTLED, ''); + Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {bankAccount, loading: false, throttledDate}); + }); } /** - * @param {Object} previousACHData - * @param {Object} data - * @returns {Object} + * @private + * @param {Number} bankAccountID */ -function setupWithdrawalAccount(previousACHData, data) { - // In Web-Secure this is referring to this.achData - still need to look at how that works... - const achData = _.extend(previousACHData, { - ...data, - isSavings: data - && !_.isUndefined(data.isSavings) - && Boolean(data.isSavings), - }); - - if (!achData.setupType) { - achData.setupType = achData.plaidAccountID - ? CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID - : CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL; - } - - let nextStep = achData.currentStep; - - API.BankAccount_SetupWithdrawal(achData) - .finally((response) => { - const currentStep = achData.currentStep; - let responseACHData = response.achData; - let error = lodashGet(responseACHData, CONST.BANK_ACCOUNT.VERIFICATIONS.ERROR_MESSAGE); - - if (response.jsonCode === 200 && !error) { - // Show warning if another account already set up this bank account and promote share - if (response.existingOwners) { - // @TODO show this error - return; - } - - // @TODO use CONST for step name - if (currentStep === CONST.BANK_ACCOUNT.STEP.REQUESTOR) { - // @TODO use CONST - const requestorResponse = lodashGet( - responseACHData, - CONST.BANK_ACCOUNT.VERIFICATIONS.REQUESTOR_IDENTITY_ID, - ); - - if (responseACHData.useOnFido) { - const onfidoResponse = lodashGet( - responseACHData, CONST.BANK_ACCOUNT.VERIFICATIONS.REQUESTOR_IDENTITY_ONFIDO, - ); - const sdkToken = lodashGet(onfidoResponse, CONST.BANK_ACCOUNT.ONFIDO_RESPONSE.SDK_TOKEN); - if (sdkToken - && !achData.isOnfidoSetupComplete - && onfidoResponse.status !== CONST.BANK_ACCOUNT.ONFIDO_RESPONSE.PASS - ) { - // Requestor Step still needs to run Onfido - responseACHData.sdkToken = sdkToken; - - // Navigate to "RequestorStep" with responseACHData - Navigation.navigate('/bank-account/requestor', responseACHData); - return; - } - } else if (requestorResponse) { - // Don't go to next step if Requestor Step needs to ask some questions - let questions = _.get(requestorResponse, CONST.BANK_ACCOUNT.QUESTIONS.QUESTION) || []; - if (_.isEmpty(questions)) { - const differentiatorQuestion = lodashGet( - requestorResponse, - CONST.BANK_ACCOUNT.QUESTIONS.DIFFERENTIATOR_QUESTION, - ); - if (differentiatorQuestion) { - questions = [differentiatorQuestion]; - } - } - if (!_.isEmpty(questions)) { - responseACHData.questions = questions; - Navigation.navigate('/bank-account/requestor', responseACHData); - return; - } - } - } - - // @TODO figure out why this is returning a promise... - if (currentStep === CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT) { - // Get an up-to-date bank account list so that we can allow the user to validate their newly - // generated bank account - return API.Get({returnValueList: 'bankAccountList'}, true) - .then((json) => { - const bankAccount = new BankAccount( - _.findWhere(json.bankAccountList, {bankAccountID: achData.bankAccountID}), - ); - responseACHData = bankAccount.toACHData(); - const needsToPassLatestChecks = responseACHData.state === BankAccount.STATE.OPEN - && responseACHData.needsToPassLatestChecks; - responseACHData.bankAccountInReview = needsToPassLatestChecks - || responseACHData.state === BankAccount.STATE.VERIFYING; - Navigation.navigate('/bank-account/validation', responseACHData); - }); - } - - if ((currentStep === CONST.BANK_ACCOUNT.STEP.VALIDATION && achData.bankAccountInReview) - || currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE - ) { - // We're done. Close the view. - // @TODO actually close it - } else { - nextStep = getNextWithdrawalAccountSetupStep(achData); - } - } else { - if (response.jsonCode === 666) { - error = response.message; - } - if (lodashGet(responseACHData, CONST.BANK_ACCOUNT.VERIFICATIONS.THROTTLED)) { - responseACHData.disableFields = true; - } - } - - goToWithdrawlAccountStep(nextStep, achData, responseACHData); - - if (error) { - // @TODO Show the error somehow - } - }); - - return achData; +function setFreePlanVerifiedBankAccountID(bankAccountID) { + Onyx.merge('freePlanBankAccount', {bankAccountID, loading: false}); + API.SetNameValuePair({name: 'expensify_freePlanBankAccountID', value: bankAccountID}); } export { @@ -499,5 +319,5 @@ export { fetchOnfidoToken, activateWallet, fetchUserWallet, - setupWithdrawalAccount, + fetchFreePlanVerifiedBankAccount, }; diff --git a/src/pages/ReimbursementAccountPage.js b/src/pages/ReimbursementAccountPage.js index d09a091d1332..d6c9a05fa4ef 100644 --- a/src/pages/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccountPage.js @@ -1,10 +1,19 @@ /* eslint-disable max-len */ /* eslint-disable react/no-unused-state */ +import _ from 'underscore'; +import moment from 'moment'; import lodashGet from 'lodash/get'; +import lodashHas from 'lodash/has'; import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import Str from 'expensify-common/lib/str'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import ScreenWrapper from '../components/ScreenWrapper'; +import {fetchFreePlanVerifiedBankAccount} from '../libs/actions/BankAccounts'; +import ONYXKEYS from '../ONYXKEYS'; +import BankAccount from '../libs/models/BankAccount'; +import * as API from '../libs/API'; const WITHDRAWAL_ACCOUNT_STEPS = [ { @@ -35,10 +44,18 @@ const WITHDRAWAL_ACCOUNT_STEPS = [ const propTypes = { skipOnfido: PropTypes.bool, + freePlanBankAccount: PropTypes.shape({ + bankAccount: PropTypes.shape({}), + loading: PropTypes.bool, + throttledDate: PropTypes.string, + }), }; const defaultProps = { skipOnfido: false, + freePlanBankAccount: { + loading: true, + }, }; class ReimbursementAccountPage extends React.Component { @@ -48,6 +65,10 @@ class ReimbursementAccountPage extends React.Component { this.state = { bankAccountID: undefined, isLoading: true, + + // In Web-Secure Plaid is disabled if there is no window.Plaid global (SDK script didn't load) or if we explicitly disable it because the user made too many calls to BankAccount_Get + // See: https://github.com/Expensify/Web-Secure/blob/044c82affb78812a58b881a6d5ba026d91dace3b/site/app/settings/reimbursement/PlaidBankForm.jsx#L171-L176 + // @TODO we should handle the too many calls to Plaid situation, but for now it will remain enabled isPlaidDisabled: false, achData: {}, isWithdrawal: true, @@ -57,10 +78,188 @@ class ReimbursementAccountPage extends React.Component { } componentDidMount() { + fetchFreePlanVerifiedBankAccount(); + } + + componentDidUpdate(prevProps) { + if (prevProps.freePlanBankAccount.loading && !this.props.freePlanBankAccount.loading) { + this.init(); + } + } + + /** + * Get step position in the array + * @param {String} stepID + * @return {Number} + */ + getIndexByStepID(stepID) { + return _.findIndex(WITHDRAWAL_ACCOUNT_STEPS, step => step.id === stepID); + } + + /** + * Get next step ID + * @return {String} + */ + getNextStepID() { + const nextStepIndex = Math.min(this.getIndexByStepID(this.state.achData.currentStep) + 1, WITHDRAWAL_ACCOUNT_STEPS.length - 1); + return lodashGet(WITHDRAWAL_ACCOUNT_STEPS, [nextStepIndex, 'id'], 'BankAccountStep'); + } + + /** + * Create or update the bank account in db with the updated data. + * + * @param {Object} [data] + */ + setupWithdrawalAccount(data) { + // @TODO trigger loading + + // Create a shallow copy of the data and overwrite new values + const newAchData = {...this.state.achData}; + _.extend(newAchData, data); + if (data && data.isSavings !== undefined) { + newAchData.isSavings = Boolean(data.isSavings); + } + if (!newAchData.setupType) { + newAchData.setupType = newAchData.plaidAccountID ? 'plaid' : 'manual'; + } + + let nextStep = newAchData.currentStep; + + // @TODO move this to an action instead + API.BankAccount_SetupWithdrawal(newAchData) + .finally((response) => { + // @TODO hide the loader + + const currentStep = newAchData.currentStep; + let achData = response.achData; + let error = lodashGet(achData, 'verifications.errorMessage'); + + if (response.jsonCode === 200 && !error) { + // Show warning if another account already set up this bank account and promote share + if (response.existingOwners) { + // @TODO Show error about existing owners + return; + } + + if (currentStep === 'RequestorStep') { + const requestorResponse = lodashGet(achData, 'verifications.externalApiResponses.requestorIdentityID'); + if (newAchData.useOnfido) { + const onfidoRes = lodashGet(achData, 'verifications.externalApiResponses.requestorIdentityOnfido'); + const sdkToken = lodashGet(onfidoRes, 'apiResult.sdkToken'); + if (sdkToken && !newAchData.isOnfidoSetupComplete && onfidoRes.status !== 'pass') { + // Requestor Step still needs to run Onfido + achData.sdkToken = sdkToken; + this.setState({achData: newAchData}, () => { + this.goToStepID('RequestorStep', achData); + }); + return; + } + } else if (requestorResponse) { + // Don't go to next step if Requestor Step needs to ask some questions + let questions = lodashGet(requestorResponse, 'apiResult.questions.question') || []; + if (_.isEmpty(questions)) { + const differentiatorQuestion = lodashGet(requestorResponse, 'apiResult.differentiator-question'); + if (differentiatorQuestion) { + questions = [differentiatorQuestion]; + } + } + if (!_.isEmpty(questions)) { + achData.questions = questions; + this.setState({achData: newAchData}, () => { + this.goToStepID('RequestorStep', achData); + }); + return; + } + } + } + if (currentStep === 'ACHContractStep') { + // const promise = $.Deferred(); // Not sure what is awaiting this promise yet but doesn't seem like something we will want to do... + + // We want to make a task completion so we can pay guides, but we don't want to close the iframe so mark completeSetup as false + // this.triggerParentSuccessMessage(response, false); - Not really sure if this applies in E.cash + + // Get an up-to-date bank account list so that we can allow the user to validate their newly generated bank account + return API.get({returnValueList: 'bankAccountList'}) + .done((json) => { + const bankAccount = new BankAccount(_.findWhere(json.bankAccountList, {bankAccountID: newAchData.bankAccountID})); + achData = bankAccount.toACHData(); + const needsToPassLatestChecks = achData.state === BankAccount.STATE.OPEN && achData.needsToPassLatestChecks; + achData.bankAccountInReview = needsToPassLatestChecks || achData.state === BankAccount.STATE.VERIFYING; + this.setState({achData: newAchData}, () => { + this.goToStepID('ValidationStep', achData); + }); + }); + } + if ((currentStep === 'ValidationStep' && newAchData.bankAccountInReview) || currentStep === 'EnableStep') { + // Setup done! We can close the modal - @TODO - not sure what this is doing or if we need it + // this.triggerParentSuccessMessage(response); + } else { + nextStep = this.getNextStepID(); + } + } else { + if (response.jsonCode === 666) { + error = response.message; + } + if (lodashGet(achData, 'verifications.throttled')) { + achData.disableFields = true; + } + } + + // Go to next step + this.setState({achData: newAchData}, () => { + this.goToStepID(nextStep, achData); + }); + + if (error) { + // @TODO - Show the error + } + }); + } + + getDefaultCountry() { + const defaultCountry = this.state.achData.country || 'US'; // @TODO - In Web-Expensify this fallback refers to User.getIpCountry() + + // In Web-Secure we check the policy to find out the defaultCountry. The policy is passed in here: + // https://github.com/Expensify/Web-Expensify/blob/896941794f68d7dce64466d83a3e86a5f8122e45/site/app/policyEditor/policyEditorPage.jsx#L2169-L2171 + // @TODO figure out whether we need to check the policy or not - not sure if it's necessary for V1 of Free Plan + return defaultCountry; + } + + /** + * Go to a specific step id. + * @param {String} stepID + * @param {Object} [achData] + */ + goToStepID(stepID, achData) { + // Setting state again will refresh the view and progress us to the next step + this.setState((prevState) => { + const newAchData = {...prevState.achData}; + + // If we go back to Requestor Step, reset any validation and previously answered questions from expectID. + if (!newAchData.useOnfido && stepID === 'RequestorStep') { + delete newAchData.questions; + delete newAchData.answers; + if (lodashHas(newAchData, 'verifications.externalApiResponses')) { + delete newAchData.verifications.externalApiResponses.requestorIdentityID; + delete newAchData.verifications.externalApiResponses.requestorIdentityKBA; + } + } + + // When going from companyStep to bankAccountStep, show the manual form instead of Plaid + if (newAchData.currentStep === 'CompanyStep' && stepID === 'BankAccountStep') { + newAchData.subStep = 'manual'; + } + + _.extend(newAchData, achData, {currentStep: stepID}); + return ({achData: newAchData}); + }); + } + + init() { // If the user already is already setting up a bank account we will want to continue the flow for them let currentStep; - const bankAccountID = parseInt(this.props.bankAccountID, 10) || 0; // This will need to come from either NVP or route param - const bankAccount = bankAccountID ? this.props.bankAccounts[bankAccountID] : null; // The list of bankAccounts will need to come from Onyx + const bankAccountJSON = parseInt(lodashGet(this.props, 'freePlanBankAccount.bankAccount.bankAccountID', '0'), 10) || 0; + const bankAccount = bankAccountJSON ? new BankAccount(bankAccountJSON) : null; const achData = bankAccount ? bankAccount.toACHData() : {}; achData.useOnfido = !this.props.skipOnfido; achData.policyID = this.props.policyId || ''; @@ -69,7 +268,10 @@ class ReimbursementAccountPage extends React.Component { achData.bankAccountInReview = bankAccount && bankAccount.isVerifying(); achData.domainLimi = 0; achData.isDomainUsingExpensifyCard = false; // Maybe also needs to be a prop... not too sure. - achData.subStep = this.props.subStep; // Unsure what the substeps are used for so far... + + // Unsure what the substeps are used for so far... pretty sure it's just to advance to the "login" step here and not used anywhere else + // https://github.com/Expensify/Web-Expensify/blob/896941794f68d7dce64466d83a3e86a5f8122e45/site/app/settings/reimbursement/bankAccountView.jsx#L356-L357 + achData.subStep = this.props.subStep; // If we're not in setup, it means we already have a withdrawal account and we're upgrading it to a business bank account. // So let the user review all steps with all info prefilled and editable, unless a specific step was passed. @@ -108,15 +310,143 @@ class ReimbursementAccountPage extends React.Component { currentStep = 'BankAccountStep'; } - console.log({currentStep, achData}); - this.setState({isLoading: false, currentStep, achData}); + this.setState({isLoading: false, currentStep, achData}, () => { + // @TODO this isn't really ideal - mostly doing this so we don't have to deviate too far from what is being migrated from Web-Secure for now. + this.goToStepID(currentStep, achData); + }); } render() { - if (this.state.isLoading) { + if (this.props.freePlanBankAccount.loading) { + return null; + } + + const defaultCountry = this.getDefaultCountry(); + const personalDetails = {firstName: this.props.personalDetails.firstName, lastName: this.props.personalDetails.lastName}; + const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, '@expensify.sms'); + + // These are all the parameters passed to React.v.AddWithdrawalAccountForm + console.debug({ + defaultCountry, + personalDetails, + userHasPhonePrimaryEmail, + achData: this.state.achData, + steps: WITHDRAWAL_ACCOUNT_STEPS, + isPlaidDisabled: this.state.isPlaidDisabled, + }); + + // React.v.AddWithdrawalAccountForm mostly exists to + // 1. show a message about being throttled + // 2. show the step progress bar (that we're not using here) + // 3. block people who have primary phone logins from adding VBA + // https://github.com/Expensify/Web-Secure/blob/044c82affb78812a58b881a6d5ba026d91dace3b/site/app/dialogs/reimbursementAccount/addWithdrawalAccountForm.jsx#L56-L73 + + if (userHasPhonePrimaryEmail) { + // @TODO message explaining that they need to make their primary login an email return null; } + // See if they is throttled + const throttledDate = lodashGet(this.props, 'freePlanBankAccount.throttledDate'); + if (throttledDate) { + const throttledEnd = moment().add(24, 'hours'); + if (moment() < throttledEnd) { + // @TODO message explaining that the user has been throttled + return null; + } + } + + // If we made it this far then we will render React.v.AddBankAccountForm with the isWithdrawal passed and the following params + console.debug({ + achData: this.state.achData, + defaultCountry, + preventCountryEdit: false, // Maybe we don't need to worry about this yet since there is no country selection? + personalDetails, + userHasPhonePrimaryEmail, + steps: WITHDRAWAL_ACCOUNT_STEPS, + isPlaidDisabled: this.state.isPlaidDisabled, + }); + + // This is where stuff gets fun... this form is used for both withdrawal and deposit accounts and displays the "step views" there are also some controls for + // navigating to next and previous steps, error, and loading states etc. Errors + loaders are set with PubSub events. We will likely want to use Onyx for that + // instead. One thing we are going to run into though is that the "stepped" view doesn't really work so great with react-navigation. But we can refactor it later + // to improve the UX and naively just swap the views for now with no transitions just to get something cooking in this big monolithic view. + + // Submitting a form - there is a global submit method that will capture the input of whatever child view is rendered... we're not gonna do that because it's + // sort of a tough pattern to understand IMO. + + // We also let a child view tell us whether it has a "nextStep" or not based on whether it has implemented a nextStep() method. This practice is also really + // strange IMO and hard to wrap one's head around. As an alternative I'd suggest that each view just fire off an action on submit. There is also something like a + // validate() class method interface that gets called on submit. We should just let each view handle it's own validation instead IMO to keep the logic in one spot. + // If something hasn't implemented the nextStep() method then we tell the controller (this component currently) to figure out what to do next + pass it the form + // values (which are also grabbed from a weird interface method). All of this stuff makes understanding the code extremely difficult so I want to basically follow + // this pattern instead of the interface/getFunctionFromDeepestView() style which is damn near impossible to reason about... + + // Each view will: + // 1. Implement it's own validation + // 2. Implement it's own submit method + // 3. Store it's form values on state + + // There are also "previousStep" methods that should now be implemented by each view and not called from a parent function but rather the view itself + + // With all of that out of the way, here's a break down of each step in the Withdrawal account flow and which methods it is implementing... + + /** + * BankAccountStep (React.v.BankAccountStep) + * - This one is tricky because it also has it's own "sub steps" and even implemented a history stack - so our "magic" methods might be found on the sub steps + * - AddBankCountry - this is a country selector that has implemented a nextStep() method so when the form is submitted it will call onCountryCurrencySubmit() + * - AddAccountNumbersManually - This is basically the manual account adding flow and it has no magic methods - but it does have form values so on submit it will + * call ReimbursementAccountPage.nextStep() which will call ReimbursementAccountPage.setupWithdrawalAccount() with those values + * - PlaidBankForm - + * - has a previousStep() implemented to either take us back to the `login` step if we are looking at the list of accounts or to call + * ReimbursementAccountPage.previousStep() - I think one way we can improve this is to just get rid of the whole next/previous step concept as controlled by + * ReimbursementAccountPage and instead just explicitly navigate to the next view from inside another one... it's all too magical! + * - Similar to the BankAccountStep itself the PlaidBankForm also has it's own "steps" which are 'accounts' and 'login' + * - PlaidLogin - offers the add method selection either plaid or manual + * - plaid + * - Login with Plaid (or switch to manual mode under certain conditions) + * - Call getPlaidBankAccounts() which calls BankAccount_Get and sets the accounts then displays the list (or handle various errors and redirects) + * - manual + * - this will send us to the manual step via "showCountryFields()" a method that literally will show the required information for a given country + * and not a selector for a country - but stuff like accountNumber, routingNumber, etc with the proper validation. If we call that function then we + * will see the AddAccountNumbersManually view. + * - PlaidAccountList - let's us choose an account from the list if we went down the plaid road. We won't find any submit buttons in here since we are + * still working with our magical functions yay! There is a nextStep() function in here that will be called on submit. It does some stuff and then we + * call ReimbursementAccountPage.setupWithdrawlAccount(). + * + * CompanyStep (React.v.CompanyStep) + * - Infinitely easier to understand here. There's just a form and no magic methods at all so you know that all we will do is call getFormValues() via FormInState + * mixin and then ReimbursementAccountPage.nextStep(). The only catch here is that there could be some validate() methods in FormInState which might not be obvious. + * + * RequestorStep (React.v.RequestorStep) + * - This one is slightly tricky as well since we will only show the Onfido SDK junk if we have a token and we only get a token when we call VerificationAPI::verify() + * with the RequestorStep and the first time we do that is when the user goes through React.v.GetRequestorIdentity + * - So, here's the whole flow broken down: + * - GetRequestorIdentity - User fills out the React.v.IdentityForm and GetRequestorIdentity.nextStep() is called on submit which does some validation stuff + * before calling ReimbursementAccountPage.nextStep() - this will ultimately hit ExpectID::verifyIDExists() and then return a bunch of questions (maybe) + * - AskRequestorIdentityQuestions - If we have questions in the response then we will have to answer them in this flow or we will use Onfido... I think...? + * This view cycles through questions/answers and then calls ReimbursementAccountPage.nextStep() again at the end. + * - Onfido SDK - this actually has no View in Web-Secure we just handle the callbacks from the SDK and once it's complete we + * call ReimbursementAccountPage.completeOnfido() - which basically just calls ReimbursementAccountPage.setupWithdrawalAccount() again with the onfidoData. + * + * ACHContractStep (React.v.BeneficialOwnersStep) + * - This is another easy one. Form values -> call nextStep() / validate -> ReimbursementAccountPage.nextStep() + * + * ValidationStep (React.v.ValidationStep) + * - This view is easy to understand as it basically looks at different things like achData.bankAccountInReview, achData.state === PENDING or state === OPEN. + * - Only if the account is PENDING do we asked the user to enter the 3 values then we call ReimbursementAccountPage.validateBankAccount() which is a separate + * API from setupWithdrawalAccount() but once we successfully validate we will call setupWithdrawalAccount() again viw nextStep() + * - The other two views will show messages and can't really be actioned on. + * + * EnableStep - We're killing this step so we don't need to worry about it. However, I think we might still need to handle it somehow in E.cash - but not entirely sure + * we'll find out more when testing. + * + * As for the whole this.achData thing if feels like the pattern of: + * - modififying the achData locally in the .then() of setupWithdrawalAccount() then "refreshing" the view + * + * Should be replaced with: + * - action called setupWithdrawalAccount() that modifies an achData key in Onyx and a larger view to render the correct steps and subscribe to this key in a dumb way + */ return ( @@ -127,4 +457,16 @@ class ReimbursementAccountPage extends React.Component { ReimbursementAccountPage.propTypes = propTypes; ReimbursementAccountPage.defaultProps = defaultProps; -export default ReimbursementAccountPage; +export default withOnyx({ + freePlanBankAccount: { + key: ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, + }, + personalDetails: { + key: ONYXKEYS.MY_PERSONAL_DETAILS, + }, + session: { + key: ONYXKEYS.SESSION, + }, + + // @TODO we maybe need the user policyID + currency + default country + whether plaid is disabled ?? +})(ReimbursementAccountPage); From 99e0f81a62513670049db51570cc241c06f5c53e Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 10 Jun 2021 20:04:00 -1000 Subject: [PATCH 06/24] move setupWithdrawalAccount into an action so that our views can purely respond to Onyx setting the achData --- src/libs/actions/BankAccounts.js | 292 +++++++++++++++++++++++- src/pages/ReimbursementAccountPage.js | 309 +++----------------------- 2 files changed, 315 insertions(+), 286 deletions(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 33c3f72f7c86..7cb8ad01f3ea 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -1,10 +1,12 @@ import lodashGet from 'lodash/get'; +import lodashHas from 'lodash/has'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import _ from 'underscore'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; +import BankAccount from '../models/BankAccount'; /** * Gets the Plaid Link token used to initialize the Plaid SDK @@ -269,6 +271,35 @@ function fetchUserWallet() { }); } +let previousACHData = {}; +Onyx.connect({ + key: ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, + callback: (val = {}) => { + previousACHData = val.achData || {}; + }, +}); + +function goToStepID(stepID, achData) { + const newACHData = {...previousACHData}; + + // If we go back to Requestor Step, reset any validation and previously answered questions from expectID. + if (!newACHData.useOnfido && stepID === 'RequestorStep') { + delete newACHData.questions; + delete newACHData.answers; + if (lodashHas(achData, 'verifications.externalApiResponses')) { + delete newACHData.verifications.externalApiResponses.requestorIdentityID; + delete newACHData.verifications.externalApiResponses.requestorIdentityKBA; + } + } + + // When going from CompanyStep to BankAccountStep, show the manual form instead of Plaid + if (newACHData.currentStep === 'CompanyStep' && stepID === 'BankAccountStep') { + newACHData.subStep = 'manual'; + } + + Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {achData: {...newACHData, ...achData, currentStep: stepID}}); +} + /** * Fetch the bank account currently being set up by the user for the free plan if it exists. */ @@ -276,6 +307,7 @@ function fetchFreePlanVerifiedBankAccount() { Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: true}); let bankAccountID; let bankAccount; + let kycVerificationsMigration; API.Get({ returnValueList: 'nameValuePairs', name: CONST.NVP.FREE_PLAN_BANK_ACCOUNT_ID, @@ -287,10 +319,24 @@ function fetchFreePlanVerifiedBankAccount() { return Promise.resolve({}); } + return API.Get({ + returnValueList: 'nameValuePairs', + name: 'expensify_migration_2020_04_28_RunKycVerifications', + }); + }) + .then((response) => { + kycVerificationsMigration = lodashGet( + response.nameValuePairs, + 'expensify_migration_2020_04_28_RunKycVerifications', + '', + ); return API.Get({returnValueList: 'bankAccountList'}); }) .then((response) => { - bankAccount = _.find(response.bankAccountList, account => account.bankAccountID === bankAccountID); + const bankAccountJSON = _.find(response.bankAccountList, account => ( + account.bankAccountID === bankAccountID + )); + bankAccount = bankAccountJSON ? new BankAccount(bankAccountJSON) : null; return API.Get({ returnValueList: 'nameValuePairs', name: CONST.NVP.ACH_DATA_THROTTLED, @@ -298,19 +344,258 @@ function fetchFreePlanVerifiedBankAccount() { }) .then((response) => { const throttledDate = lodashGet(response.nameValuePairs, CONST.NVP.ACH_DATA_THROTTLED, ''); - Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {bankAccount, loading: false, throttledDate}); + + // Next we'll build the achData and save it to Onyx + // If the user already is already setting up a bank account we will want to continue the flow for them + let currentStep; + const achData = bankAccount ? bankAccount.toACHData() : {}; + achData.useOnfido = true; + achData.policyID = ''; + + // @TODO I think probably we won't set the token until we actually ask to start the the Plaid flow + // but we could also get it here with another API call... + achData.plaidLinkToken = ''; + achData.isInSetup = !bankAccount || bankAccount.isInSetup(); + achData.bankAccountInReview = bankAccount && bankAccount.isVerifying(); + achData.domainLimit = 0; + achData.isDomainUsingExpensifyCard = false; // @TODO - Not actually sure if we need this + + // @TODO This subStep is used to either show the Plaid "login" view or the "manual" view - but not sure if + // we need to implement it yet... + // eslint-disable-next-line max-len + // See Web-Secure: https://github.com/Expensify/Web-Expensify/blob/896941794f68d7dce64466d83a3e86a5f8122e45/site/app/settings/reimbursement/bankAccountView.jsx#L356-L357 + achData.subStep = ''; + + // If we're not in setup, it means we already have a withdrawal account and we're upgrading it to a business + // bank account. So let the user review all steps with all info prefilled and editable, unless a specific + // step was passed. + if (!achData.isInSetup) { + // @TODO Not sure if we need to do this since for NewDot none of the accounts are pre-existing ones + currentStep = ''; + } + + // Temporary fix for Onfido flow. Can be removed by nkuoch after Sept 1 2020. + // @TODO not sure if we still need this or what this is about, but seems like maybe yes... + if (currentStep === 'ACHContractStep' && achData.useOnfido) { + const onfidoRes = lodashGet(achData, 'verifications.externalApiResponses.requestorIdentityOnfido'); + const sdkToken = lodashGet(onfidoRes, 'apiResult.sdkToken'); + if (sdkToken && !achData.isOnfidoSetupComplete && onfidoRes.status !== 'pass') { + currentStep = 'RequestorStep'; + } + } + + // Ensure we route the user to the correct step based on the status of their bank account + if (bankAccount && !currentStep) { + currentStep = bankAccount.isPending() + || bankAccount.isVerifying() ? 'ValidationStep' : 'BankAccountStep'; + + // @TODO Again, not sure how much of this logic is needed right now as we shouldn't be handling any + // open accounts in E.cash yet that need to pass any more checks or can be upgraded, but leaving in for + // possible future compatibility. + if (bankAccount.isOpen()) { + if (bankAccount.needsToPassLatestChecks()) { + const hasTriedToUpgrade = bankAccount.getDateSigned() + > (kycVerificationsMigration || '2020-01-13'); + currentStep = hasTriedToUpgrade ? 'ValidationStep' : 'CompanyStep'; + achData.bankAccountInReview = hasTriedToUpgrade; + } else { + // We're not going to have a EnableStep, but we can show this account as open if accessed + currentStep = 'EnableStep'; + } + } + } + + // If at this point we still don't have a current step, default to the BankAccountStep + if (!currentStep) { + currentStep = 'BankAccountStep'; + } + + Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {throttledDate}); + goToStepID(currentStep, achData); + }) + .finally(() => { + Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: false}); }); } +const WITHDRAWAL_ACCOUNT_STEPS = [ + { + id: 'BankAccountStep', + title: 'Bank Account', + }, + { + id: 'CompanyStep', + title: 'Company Information', + }, + { + id: 'RequestorStep', + title: 'Requestor Information', + }, + { + id: 'ACHContractStep', + title: 'Beneficial Owners', + }, + { + id: 'ValidationStep', + title: 'Validate', + }, + { + id: 'EnableStep', + title: 'Enable', + }, +]; + +/** + * Get step position in the array + * @param {String} stepID + * @return {Number} + */ +function getIndexByStepID(stepID) { + return _.findIndex(WITHDRAWAL_ACCOUNT_STEPS, step => step.id === stepID); +} + +/** + * Get next step ID + * @return {String} + */ +function getNextStepID() { + const nextStepIndex = Math.min( + getIndexByStepID(this.state.achData.currentStep) + 1, + WITHDRAWAL_ACCOUNT_STEPS.length - 1, + ); + return lodashGet(WITHDRAWAL_ACCOUNT_STEPS, [nextStepIndex, 'id'], 'BankAccountStep'); +} + + /** * @private * @param {Number} bankAccountID */ function setFreePlanVerifiedBankAccountID(bankAccountID) { - Onyx.merge('freePlanBankAccount', {bankAccountID, loading: false}); API.SetNameValuePair({name: 'expensify_freePlanBankAccountID', value: bankAccountID}); } +/** + * Create or update the bank account in db with the updated data. + * + * @param {Object} [data] + */ +function setupWithdrawalAccount(data) { + Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: true}); + + previousACHData = {...previousACHData, ...data}; + if (data && data.isSavings !== undefined) { + previousACHData.isSavings = Boolean(data.isSavings); + } + if (!previousACHData.setupType) { + previousACHData.setupType = previousACHData.plaidAccountID ? 'plaid' : 'manual'; + } + + let nextStep = previousACHData.currentStep; + + API.BankAccount_SetupWithdrawal(previousACHData) + .finally((response) => { + Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: false}); + + const currentStep = previousACHData.currentStep; + let achData = response.achData; + let error = lodashGet(achData, 'verifications.errorMessage'); + + if (response.jsonCode === 200 && !error) { + // Save an NVP with the bankAccountID for this account. This is temporary since we are not showing lists + // of accounts yet and must have some kind of record of which account is the one the user is trying to + // set up for the free plan. + setFreePlanVerifiedBankAccountID(achData.bankAccountID); + + // Show warning if another account already set up this bank account and promote share + if (response.existingOwners) { + // @TODO Show better error in UI about existing owners + console.error('Cannot set up withdrawal account due to existing owners'); + return; + } + + if (currentStep === 'RequestorStep') { + const requestorResponse = lodashGet( + achData, + 'verifications.externalApiResponses.requestorIdentityID', + ); + if (previousACHData.useOnfido) { + const onfidoRes = lodashGet( + achData, + 'verifications.externalApiResponses.requestorIdentityOnfido', + ); + const sdkToken = lodashGet(onfidoRes, 'apiResult.sdkToken'); + if (sdkToken && !previousACHData.isOnfidoSetupComplete && onfidoRes.status !== 'pass') { + // Requestor Step still needs to run Onfido + achData.sdkToken = sdkToken; + goToStepID('RequestorStep', achData); + return; + } + } else if (requestorResponse) { + // Don't go to next step if Requestor Step needs to ask some questions + let questions = lodashGet(requestorResponse, 'apiResult.questions.question') || []; + if (_.isEmpty(questions)) { + const differentiatorQuestion = lodashGet( + requestorResponse, + 'apiResult.differentiator-question', + ); + if (differentiatorQuestion) { + questions = [differentiatorQuestion]; + } + } + if (!_.isEmpty(questions)) { + achData.questions = questions; + goToStepID('RequestorStep', achData); + return; + } + } + } + + if (currentStep === 'ACHContractStep') { + // Get an up-to-date bank account list so that we can allow the user to validate their newly + // generated bank account + return API.get({returnValueList: 'bankAccountList'}) + .done((json) => { + const bankAccountJSON = _.findWhere(json.bankAccountList, { + bankAccountID: previousACHData.bankAccountID, + }); + const bankAccount = new BankAccount(bankAccountJSON); + achData = bankAccount.toACHData(); + const needsToPassLatestChecks = achData.state === BankAccount.STATE.OPEN + && achData.needsToPassLatestChecks; + achData.bankAccountInReview = needsToPassLatestChecks + || achData.state === BankAccount.STATE.VERIFYING; + + goToStepID('ValidationStep', achData); + }); + } + + if ((currentStep === 'ValidationStep' && previousACHData.bankAccountInReview) + || currentStep === 'EnableStep' + ) { + // Setup done! + } else { + nextStep = getNextStepID(); + } + } else { + if (response.jsonCode === 666) { + error = response.message; + } + if (lodashGet(achData, 'verifications.throttled')) { + achData.disableFields = true; + } + } + + // Go to next step + goToStepID(nextStep, achData); + + if (error) { + // @TODO - Show the error for real + console.error(`Error setting up withdrawal account: ${error}`); + } + }); +} + export { fetchPlaidLinkToken, addPersonalBankAccount, @@ -320,4 +605,5 @@ export { activateWallet, fetchUserWallet, fetchFreePlanVerifiedBankAccount, + setupWithdrawalAccount, }; diff --git a/src/pages/ReimbursementAccountPage.js b/src/pages/ReimbursementAccountPage.js index d6c9a05fa4ef..da990bdf32c8 100644 --- a/src/pages/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccountPage.js @@ -1,9 +1,7 @@ /* eslint-disable max-len */ /* eslint-disable react/no-unused-state */ -import _ from 'underscore'; import moment from 'moment'; import lodashGet from 'lodash/get'; -import lodashHas from 'lodash/has'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; @@ -12,50 +10,31 @@ import PropTypes from 'prop-types'; import ScreenWrapper from '../components/ScreenWrapper'; import {fetchFreePlanVerifiedBankAccount} from '../libs/actions/BankAccounts'; import ONYXKEYS from '../ONYXKEYS'; -import BankAccount from '../libs/models/BankAccount'; -import * as API from '../libs/API'; - -const WITHDRAWAL_ACCOUNT_STEPS = [ - { - id: 'BankAccountStep', - title: 'Bank Account', - }, - { - id: 'CompanyStep', - title: 'Company Information', - }, - { - id: 'RequestorStep', - title: 'Requestor Information', - }, - { - id: 'ACHContractStep', - title: 'Beneficial Owners', - }, - { - id: 'ValidationStep', - title: 'Validate', - }, - { - id: 'EnableStep', - title: 'Enable', - }, -]; const propTypes = { - skipOnfido: PropTypes.bool, freePlanBankAccount: PropTypes.shape({ - bankAccount: PropTypes.shape({}), loading: PropTypes.bool, throttledDate: PropTypes.string, + achData: PropTypes.shape({ + country: PropTypes.string, + }), + }), + + personalDetails: PropTypes.shape({ + firstName: PropTypes.string, + lastName: PropTypes.string, }), + + session: PropTypes.shape({ + email: PropTypes.string, + }).isRequired, }; const defaultProps = { - skipOnfido: false, freePlanBankAccount: { loading: true, }, + personalDetails: {}, }; class ReimbursementAccountPage extends React.Component { @@ -65,11 +44,6 @@ class ReimbursementAccountPage extends React.Component { this.state = { bankAccountID: undefined, isLoading: true, - - // In Web-Secure Plaid is disabled if there is no window.Plaid global (SDK script didn't load) or if we explicitly disable it because the user made too many calls to BankAccount_Get - // See: https://github.com/Expensify/Web-Secure/blob/044c82affb78812a58b881a6d5ba026d91dace3b/site/app/settings/reimbursement/PlaidBankForm.jsx#L171-L176 - // @TODO we should handle the too many calls to Plaid situation, but for now it will remain enabled - isPlaidDisabled: false, achData: {}, isWithdrawal: true, lastDataWithError: {}, @@ -81,247 +55,17 @@ class ReimbursementAccountPage extends React.Component { fetchFreePlanVerifiedBankAccount(); } - componentDidUpdate(prevProps) { - if (prevProps.freePlanBankAccount.loading && !this.props.freePlanBankAccount.loading) { - this.init(); - } - } - - /** - * Get step position in the array - * @param {String} stepID - * @return {Number} - */ - getIndexByStepID(stepID) { - return _.findIndex(WITHDRAWAL_ACCOUNT_STEPS, step => step.id === stepID); - } - - /** - * Get next step ID - * @return {String} - */ - getNextStepID() { - const nextStepIndex = Math.min(this.getIndexByStepID(this.state.achData.currentStep) + 1, WITHDRAWAL_ACCOUNT_STEPS.length - 1); - return lodashGet(WITHDRAWAL_ACCOUNT_STEPS, [nextStepIndex, 'id'], 'BankAccountStep'); - } - - /** - * Create or update the bank account in db with the updated data. - * - * @param {Object} [data] - */ - setupWithdrawalAccount(data) { - // @TODO trigger loading - - // Create a shallow copy of the data and overwrite new values - const newAchData = {...this.state.achData}; - _.extend(newAchData, data); - if (data && data.isSavings !== undefined) { - newAchData.isSavings = Boolean(data.isSavings); - } - if (!newAchData.setupType) { - newAchData.setupType = newAchData.plaidAccountID ? 'plaid' : 'manual'; + render() { + if (this.props.freePlanBankAccount.loading) { + return null; } - let nextStep = newAchData.currentStep; - - // @TODO move this to an action instead - API.BankAccount_SetupWithdrawal(newAchData) - .finally((response) => { - // @TODO hide the loader - - const currentStep = newAchData.currentStep; - let achData = response.achData; - let error = lodashGet(achData, 'verifications.errorMessage'); - - if (response.jsonCode === 200 && !error) { - // Show warning if another account already set up this bank account and promote share - if (response.existingOwners) { - // @TODO Show error about existing owners - return; - } - - if (currentStep === 'RequestorStep') { - const requestorResponse = lodashGet(achData, 'verifications.externalApiResponses.requestorIdentityID'); - if (newAchData.useOnfido) { - const onfidoRes = lodashGet(achData, 'verifications.externalApiResponses.requestorIdentityOnfido'); - const sdkToken = lodashGet(onfidoRes, 'apiResult.sdkToken'); - if (sdkToken && !newAchData.isOnfidoSetupComplete && onfidoRes.status !== 'pass') { - // Requestor Step still needs to run Onfido - achData.sdkToken = sdkToken; - this.setState({achData: newAchData}, () => { - this.goToStepID('RequestorStep', achData); - }); - return; - } - } else if (requestorResponse) { - // Don't go to next step if Requestor Step needs to ask some questions - let questions = lodashGet(requestorResponse, 'apiResult.questions.question') || []; - if (_.isEmpty(questions)) { - const differentiatorQuestion = lodashGet(requestorResponse, 'apiResult.differentiator-question'); - if (differentiatorQuestion) { - questions = [differentiatorQuestion]; - } - } - if (!_.isEmpty(questions)) { - achData.questions = questions; - this.setState({achData: newAchData}, () => { - this.goToStepID('RequestorStep', achData); - }); - return; - } - } - } - if (currentStep === 'ACHContractStep') { - // const promise = $.Deferred(); // Not sure what is awaiting this promise yet but doesn't seem like something we will want to do... - - // We want to make a task completion so we can pay guides, but we don't want to close the iframe so mark completeSetup as false - // this.triggerParentSuccessMessage(response, false); - Not really sure if this applies in E.cash - - // Get an up-to-date bank account list so that we can allow the user to validate their newly generated bank account - return API.get({returnValueList: 'bankAccountList'}) - .done((json) => { - const bankAccount = new BankAccount(_.findWhere(json.bankAccountList, {bankAccountID: newAchData.bankAccountID})); - achData = bankAccount.toACHData(); - const needsToPassLatestChecks = achData.state === BankAccount.STATE.OPEN && achData.needsToPassLatestChecks; - achData.bankAccountInReview = needsToPassLatestChecks || achData.state === BankAccount.STATE.VERIFYING; - this.setState({achData: newAchData}, () => { - this.goToStepID('ValidationStep', achData); - }); - }); - } - if ((currentStep === 'ValidationStep' && newAchData.bankAccountInReview) || currentStep === 'EnableStep') { - // Setup done! We can close the modal - @TODO - not sure what this is doing or if we need it - // this.triggerParentSuccessMessage(response); - } else { - nextStep = this.getNextStepID(); - } - } else { - if (response.jsonCode === 666) { - error = response.message; - } - if (lodashGet(achData, 'verifications.throttled')) { - achData.disableFields = true; - } - } - - // Go to next step - this.setState({achData: newAchData}, () => { - this.goToStepID(nextStep, achData); - }); - - if (error) { - // @TODO - Show the error - } - }); - } - - getDefaultCountry() { - const defaultCountry = this.state.achData.country || 'US'; // @TODO - In Web-Expensify this fallback refers to User.getIpCountry() + const {achData} = this.props.freePlanBankAccount; // In Web-Secure we check the policy to find out the defaultCountry. The policy is passed in here: // https://github.com/Expensify/Web-Expensify/blob/896941794f68d7dce64466d83a3e86a5f8122e45/site/app/policyEditor/policyEditorPage.jsx#L2169-L2171 // @TODO figure out whether we need to check the policy or not - not sure if it's necessary for V1 of Free Plan - return defaultCountry; - } - - /** - * Go to a specific step id. - * @param {String} stepID - * @param {Object} [achData] - */ - goToStepID(stepID, achData) { - // Setting state again will refresh the view and progress us to the next step - this.setState((prevState) => { - const newAchData = {...prevState.achData}; - - // If we go back to Requestor Step, reset any validation and previously answered questions from expectID. - if (!newAchData.useOnfido && stepID === 'RequestorStep') { - delete newAchData.questions; - delete newAchData.answers; - if (lodashHas(newAchData, 'verifications.externalApiResponses')) { - delete newAchData.verifications.externalApiResponses.requestorIdentityID; - delete newAchData.verifications.externalApiResponses.requestorIdentityKBA; - } - } - - // When going from companyStep to bankAccountStep, show the manual form instead of Plaid - if (newAchData.currentStep === 'CompanyStep' && stepID === 'BankAccountStep') { - newAchData.subStep = 'manual'; - } - - _.extend(newAchData, achData, {currentStep: stepID}); - return ({achData: newAchData}); - }); - } - - init() { - // If the user already is already setting up a bank account we will want to continue the flow for them - let currentStep; - const bankAccountJSON = parseInt(lodashGet(this.props, 'freePlanBankAccount.bankAccount.bankAccountID', '0'), 10) || 0; - const bankAccount = bankAccountJSON ? new BankAccount(bankAccountJSON) : null; - const achData = bankAccount ? bankAccount.toACHData() : {}; - achData.useOnfido = !this.props.skipOnfido; - achData.policyID = this.props.policyId || ''; - achData.plaidLinkToken = this.props.plaidLinkToken; // I think probably we won't have this until we start the Plaid flow but maybe...? - achData.isInSetup = !bankAccount || bankAccount.isInSetup(); - achData.bankAccountInReview = bankAccount && bankAccount.isVerifying(); - achData.domainLimi = 0; - achData.isDomainUsingExpensifyCard = false; // Maybe also needs to be a prop... not too sure. - - // Unsure what the substeps are used for so far... pretty sure it's just to advance to the "login" step here and not used anywhere else - // https://github.com/Expensify/Web-Expensify/blob/896941794f68d7dce64466d83a3e86a5f8122e45/site/app/settings/reimbursement/bankAccountView.jsx#L356-L357 - achData.subStep = this.props.subStep; - - // If we're not in setup, it means we already have a withdrawal account and we're upgrading it to a business bank account. - // So let the user review all steps with all info prefilled and editable, unless a specific step was passed. - if (!achData.isInSetup) { - currentStep = this.props.currentStep; // Not sure if we need this as user's won't be upgrading these accounts... - } - - // Temporary fix for Onfido flow. Can be removed by nkuoch after Sept 1 2020. - not sure if we need this or what this is about... - if (currentStep === 'ACHContractStep' && achData.useOnfido) { - const onfidoRes = lodashGet(achData, 'verifications.externalApiResponses.requestorIdentityOnfido'); - const sdkToken = lodashGet(onfidoRes, 'apiResult.sdkToken'); - if (sdkToken && !achData.isOnfidoSetupComplete && onfidoRes.status !== 'pass') { - currentStep = 'RequestorStep'; - } - } - - // Ensure we route the user to the correct step based on the status of their bank account - if (bankAccount && !currentStep) { - currentStep = bankAccount.isPending() || bankAccount.isVerifying() ? 'ValidationStep' : 'BankAccountStep'; - - // Again, not sure how much of this logic is needed right now as we shouldn't be handling any open accounts in E.cash yet... - if (bankAccount.isOpen()) { - if (bankAccount.needsToPassLatestChecks()) { - // const hasTriedToUpgrade = bankAccount.getDateSigned() > (NVP.get('expensify_migration_2020_04_28_RunKycVerifications') || '2020-01-13'); - // currentStep = hasTriedToUpgrade ? 'ValidationStep' : 'CompanyStep'; - // achData.bankAccountInReview = hasTriedToUpgrade; - } else { - // Not handling the EnableStep... - // currentStep = 'EnableStep'; - } - } - } - - // If at this point we still don't have a current step, default to the BankAccountStep - if (!currentStep) { - currentStep = 'BankAccountStep'; - } - - this.setState({isLoading: false, currentStep, achData}, () => { - // @TODO this isn't really ideal - mostly doing this so we don't have to deviate too far from what is being migrated from Web-Secure for now. - this.goToStepID(currentStep, achData); - }); - } - - render() { - if (this.props.freePlanBankAccount.loading) { - return null; - } - - const defaultCountry = this.getDefaultCountry(); + const defaultCountry = achData.country || 'US'; // @TODO - In Web-Expensify this fallback refers to User.getIpCountry() const personalDetails = {firstName: this.props.personalDetails.firstName, lastName: this.props.personalDetails.lastName}; const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, '@expensify.sms'); @@ -330,15 +74,13 @@ class ReimbursementAccountPage extends React.Component { defaultCountry, personalDetails, userHasPhonePrimaryEmail, - achData: this.state.achData, - steps: WITHDRAWAL_ACCOUNT_STEPS, - isPlaidDisabled: this.state.isPlaidDisabled, + achData, }); // React.v.AddWithdrawalAccountForm mostly exists to - // 1. show a message about being throttled - // 2. show the step progress bar (that we're not using here) - // 3. block people who have primary phone logins from adding VBA + // 1. Show a message about being throttled + // 2. Show the step progress bar (that we're not using here) + // 3. Block people who have primary phone logins from adding VBA // https://github.com/Expensify/Web-Secure/blob/044c82affb78812a58b881a6d5ba026d91dace3b/site/app/dialogs/reimbursementAccount/addWithdrawalAccountForm.jsx#L56-L73 if (userHasPhonePrimaryEmail) { @@ -358,15 +100,16 @@ class ReimbursementAccountPage extends React.Component { // If we made it this far then we will render React.v.AddBankAccountForm with the isWithdrawal passed and the following params console.debug({ - achData: this.state.achData, + achData, defaultCountry, preventCountryEdit: false, // Maybe we don't need to worry about this yet since there is no country selection? personalDetails, userHasPhonePrimaryEmail, - steps: WITHDRAWAL_ACCOUNT_STEPS, - isPlaidDisabled: this.state.isPlaidDisabled, }); + // Everything we need to display UI-wise is pretty much in the achData at this point. We just need to call the correct actions in the right places when + // submitting a form or navigating back or forward. + // This is where stuff gets fun... this form is used for both withdrawal and deposit accounts and displays the "step views" there are also some controls for // navigating to next and previous steps, error, and loading states etc. Errors + loaders are set with PubSub events. We will likely want to use Onyx for that // instead. One thing we are going to run into though is that the "stepped" view doesn't really work so great with react-navigation. But we can refactor it later From 3319c162894e776dcc2e66ae982eedbc40366af0 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 10 Jun 2021 20:14:38 -1000 Subject: [PATCH 07/24] move everything into actions clean up temp view --- src/ONYXKEYS.js | 4 +- src/libs/actions/BankAccounts.js | 14 +-- src/pages/ReimbursementAccountPage.js | 135 ++------------------------ 3 files changed, 19 insertions(+), 134 deletions(-) diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index b3271f8b3911..f585c8116a05 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -100,6 +100,6 @@ export default { // Object containing Wallet terms step state WALLET_TERMS: 'walletTerms', - // Stores information about the free plan bank account being set up - FREE_PLAN_BANK_ACCOUNT: 'freePlanBankAccount', + // Stores information about the active reimbursement account being set up + REIMBURSEMENT_ACCOUNT: 'reimbursementAccount', }; diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 7cb8ad01f3ea..e136e468f010 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -273,7 +273,7 @@ function fetchUserWallet() { let previousACHData = {}; Onyx.connect({ - key: ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, callback: (val = {}) => { previousACHData = val.achData || {}; }, @@ -297,14 +297,14 @@ function goToStepID(stepID, achData) { newACHData.subStep = 'manual'; } - Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {achData: {...newACHData, ...achData, currentStep: stepID}}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {...newACHData, ...achData, currentStep: stepID}}); } /** * Fetch the bank account currently being set up by the user for the free plan if it exists. */ function fetchFreePlanVerifiedBankAccount() { - Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: true}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: true}); let bankAccountID; let bankAccount; let kycVerificationsMigration; @@ -410,11 +410,11 @@ function fetchFreePlanVerifiedBankAccount() { currentStep = 'BankAccountStep'; } - Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {throttledDate}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {throttledDate}); goToStepID(currentStep, achData); }) .finally(() => { - Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: false}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); }); } @@ -481,7 +481,7 @@ function setFreePlanVerifiedBankAccountID(bankAccountID) { * @param {Object} [data] */ function setupWithdrawalAccount(data) { - Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: true}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: true}); previousACHData = {...previousACHData, ...data}; if (data && data.isSavings !== undefined) { @@ -495,7 +495,7 @@ function setupWithdrawalAccount(data) { API.BankAccount_SetupWithdrawal(previousACHData) .finally((response) => { - Onyx.merge(ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, {loading: false}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); const currentStep = previousACHData.currentStep; let achData = response.achData; diff --git a/src/pages/ReimbursementAccountPage.js b/src/pages/ReimbursementAccountPage.js index da990bdf32c8..a69c70f7d342 100644 --- a/src/pages/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccountPage.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ /* eslint-disable max-len */ /* eslint-disable react/no-unused-state */ import moment from 'moment'; @@ -10,9 +11,10 @@ import PropTypes from 'prop-types'; import ScreenWrapper from '../components/ScreenWrapper'; import {fetchFreePlanVerifiedBankAccount} from '../libs/actions/BankAccounts'; import ONYXKEYS from '../ONYXKEYS'; +import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator'; const propTypes = { - freePlanBankAccount: PropTypes.shape({ + reimbursementAccount: PropTypes.shape({ loading: PropTypes.bool, throttledDate: PropTypes.string, achData: PropTypes.shape({ @@ -31,36 +33,23 @@ const propTypes = { }; const defaultProps = { - freePlanBankAccount: { + reimbursementAccount: { loading: true, }, personalDetails: {}, }; class ReimbursementAccountPage extends React.Component { - constructor(props) { - super(props); - - this.state = { - bankAccountID: undefined, - isLoading: true, - achData: {}, - isWithdrawal: true, - lastDataWithError: {}, - currentStep: 'BankAccountStep', - }; - } - componentDidMount() { fetchFreePlanVerifiedBankAccount(); } render() { - if (this.props.freePlanBankAccount.loading) { - return null; + if (this.props.reimbursementAccount.loading) { + return ; } - const {achData} = this.props.freePlanBankAccount; + const {achData} = this.props.reimbursementAccount; // In Web-Secure we check the policy to find out the defaultCountry. The policy is passed in here: // https://github.com/Expensify/Web-Expensify/blob/896941794f68d7dce64466d83a3e86a5f8122e45/site/app/policyEditor/policyEditorPage.jsx#L2169-L2171 @@ -69,27 +58,13 @@ class ReimbursementAccountPage extends React.Component { const personalDetails = {firstName: this.props.personalDetails.firstName, lastName: this.props.personalDetails.lastName}; const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, '@expensify.sms'); - // These are all the parameters passed to React.v.AddWithdrawalAccountForm - console.debug({ - defaultCountry, - personalDetails, - userHasPhonePrimaryEmail, - achData, - }); - - // React.v.AddWithdrawalAccountForm mostly exists to - // 1. Show a message about being throttled - // 2. Show the step progress bar (that we're not using here) - // 3. Block people who have primary phone logins from adding VBA - // https://github.com/Expensify/Web-Secure/blob/044c82affb78812a58b881a6d5ba026d91dace3b/site/app/dialogs/reimbursementAccount/addWithdrawalAccountForm.jsx#L56-L73 - if (userHasPhonePrimaryEmail) { // @TODO message explaining that they need to make their primary login an email return null; } // See if they is throttled - const throttledDate = lodashGet(this.props, 'freePlanBankAccount.throttledDate'); + const throttledDate = lodashGet(this.props, 'reimbursementAccount.throttledDate'); if (throttledDate) { const throttledEnd = moment().add(24, 'hours'); if (moment() < throttledEnd) { @@ -98,98 +73,8 @@ class ReimbursementAccountPage extends React.Component { } } - // If we made it this far then we will render React.v.AddBankAccountForm with the isWithdrawal passed and the following params - console.debug({ - achData, - defaultCountry, - preventCountryEdit: false, // Maybe we don't need to worry about this yet since there is no country selection? - personalDetails, - userHasPhonePrimaryEmail, - }); - // Everything we need to display UI-wise is pretty much in the achData at this point. We just need to call the correct actions in the right places when // submitting a form or navigating back or forward. - - // This is where stuff gets fun... this form is used for both withdrawal and deposit accounts and displays the "step views" there are also some controls for - // navigating to next and previous steps, error, and loading states etc. Errors + loaders are set with PubSub events. We will likely want to use Onyx for that - // instead. One thing we are going to run into though is that the "stepped" view doesn't really work so great with react-navigation. But we can refactor it later - // to improve the UX and naively just swap the views for now with no transitions just to get something cooking in this big monolithic view. - - // Submitting a form - there is a global submit method that will capture the input of whatever child view is rendered... we're not gonna do that because it's - // sort of a tough pattern to understand IMO. - - // We also let a child view tell us whether it has a "nextStep" or not based on whether it has implemented a nextStep() method. This practice is also really - // strange IMO and hard to wrap one's head around. As an alternative I'd suggest that each view just fire off an action on submit. There is also something like a - // validate() class method interface that gets called on submit. We should just let each view handle it's own validation instead IMO to keep the logic in one spot. - // If something hasn't implemented the nextStep() method then we tell the controller (this component currently) to figure out what to do next + pass it the form - // values (which are also grabbed from a weird interface method). All of this stuff makes understanding the code extremely difficult so I want to basically follow - // this pattern instead of the interface/getFunctionFromDeepestView() style which is damn near impossible to reason about... - - // Each view will: - // 1. Implement it's own validation - // 2. Implement it's own submit method - // 3. Store it's form values on state - - // There are also "previousStep" methods that should now be implemented by each view and not called from a parent function but rather the view itself - - // With all of that out of the way, here's a break down of each step in the Withdrawal account flow and which methods it is implementing... - - /** - * BankAccountStep (React.v.BankAccountStep) - * - This one is tricky because it also has it's own "sub steps" and even implemented a history stack - so our "magic" methods might be found on the sub steps - * - AddBankCountry - this is a country selector that has implemented a nextStep() method so when the form is submitted it will call onCountryCurrencySubmit() - * - AddAccountNumbersManually - This is basically the manual account adding flow and it has no magic methods - but it does have form values so on submit it will - * call ReimbursementAccountPage.nextStep() which will call ReimbursementAccountPage.setupWithdrawalAccount() with those values - * - PlaidBankForm - - * - has a previousStep() implemented to either take us back to the `login` step if we are looking at the list of accounts or to call - * ReimbursementAccountPage.previousStep() - I think one way we can improve this is to just get rid of the whole next/previous step concept as controlled by - * ReimbursementAccountPage and instead just explicitly navigate to the next view from inside another one... it's all too magical! - * - Similar to the BankAccountStep itself the PlaidBankForm also has it's own "steps" which are 'accounts' and 'login' - * - PlaidLogin - offers the add method selection either plaid or manual - * - plaid - * - Login with Plaid (or switch to manual mode under certain conditions) - * - Call getPlaidBankAccounts() which calls BankAccount_Get and sets the accounts then displays the list (or handle various errors and redirects) - * - manual - * - this will send us to the manual step via "showCountryFields()" a method that literally will show the required information for a given country - * and not a selector for a country - but stuff like accountNumber, routingNumber, etc with the proper validation. If we call that function then we - * will see the AddAccountNumbersManually view. - * - PlaidAccountList - let's us choose an account from the list if we went down the plaid road. We won't find any submit buttons in here since we are - * still working with our magical functions yay! There is a nextStep() function in here that will be called on submit. It does some stuff and then we - * call ReimbursementAccountPage.setupWithdrawlAccount(). - * - * CompanyStep (React.v.CompanyStep) - * - Infinitely easier to understand here. There's just a form and no magic methods at all so you know that all we will do is call getFormValues() via FormInState - * mixin and then ReimbursementAccountPage.nextStep(). The only catch here is that there could be some validate() methods in FormInState which might not be obvious. - * - * RequestorStep (React.v.RequestorStep) - * - This one is slightly tricky as well since we will only show the Onfido SDK junk if we have a token and we only get a token when we call VerificationAPI::verify() - * with the RequestorStep and the first time we do that is when the user goes through React.v.GetRequestorIdentity - * - So, here's the whole flow broken down: - * - GetRequestorIdentity - User fills out the React.v.IdentityForm and GetRequestorIdentity.nextStep() is called on submit which does some validation stuff - * before calling ReimbursementAccountPage.nextStep() - this will ultimately hit ExpectID::verifyIDExists() and then return a bunch of questions (maybe) - * - AskRequestorIdentityQuestions - If we have questions in the response then we will have to answer them in this flow or we will use Onfido... I think...? - * This view cycles through questions/answers and then calls ReimbursementAccountPage.nextStep() again at the end. - * - Onfido SDK - this actually has no View in Web-Secure we just handle the callbacks from the SDK and once it's complete we - * call ReimbursementAccountPage.completeOnfido() - which basically just calls ReimbursementAccountPage.setupWithdrawalAccount() again with the onfidoData. - * - * ACHContractStep (React.v.BeneficialOwnersStep) - * - This is another easy one. Form values -> call nextStep() / validate -> ReimbursementAccountPage.nextStep() - * - * ValidationStep (React.v.ValidationStep) - * - This view is easy to understand as it basically looks at different things like achData.bankAccountInReview, achData.state === PENDING or state === OPEN. - * - Only if the account is PENDING do we asked the user to enter the 3 values then we call ReimbursementAccountPage.validateBankAccount() which is a separate - * API from setupWithdrawalAccount() but once we successfully validate we will call setupWithdrawalAccount() again viw nextStep() - * - The other two views will show messages and can't really be actioned on. - * - * EnableStep - We're killing this step so we don't need to worry about it. However, I think we might still need to handle it somehow in E.cash - but not entirely sure - * we'll find out more when testing. - * - * As for the whole this.achData thing if feels like the pattern of: - * - modififying the achData locally in the .then() of setupWithdrawalAccount() then "refreshing" the view - * - * Should be replaced with: - * - action called setupWithdrawalAccount() that modifies an achData key in Onyx and a larger view to render the correct steps and subscribe to this key in a dumb way - */ return ( @@ -201,8 +86,8 @@ class ReimbursementAccountPage extends React.Component { ReimbursementAccountPage.propTypes = propTypes; ReimbursementAccountPage.defaultProps = defaultProps; export default withOnyx({ - freePlanBankAccount: { - key: ONYXKEYS.FREE_PLAN_BANK_ACCOUNT, + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, personalDetails: { key: ONYXKEYS.MY_PERSONAL_DETAILS, From d61a5255e6d3edab112b26f1af73b5503216cf47 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 11 Jun 2021 09:08:15 -1000 Subject: [PATCH 08/24] fix conflicts and rename some things --- src/ROUTES.js | 2 +- src/libs/Navigation/AppNavigator/AuthScreens.js | 7 ------- .../Navigation/AppNavigator/ModalStackNavigators.js | 8 -------- src/libs/Navigation/linkingConfig.js | 7 +------ src/pages/BusinessBankAccount/NewPage.js | 10 +++++----- 5 files changed, 7 insertions(+), 27 deletions(-) diff --git a/src/ROUTES.js b/src/ROUTES.js index 8fbb80f1853c..9669366fec88 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -9,7 +9,7 @@ const REPORT = 'r'; export default { ADD_PERSONAL_BANK_ACCOUNT: 'add-personal-bank-account', - BANK_ACCOUNT_NEW: 'bank-account/new', + ADD_VERIFIED_BANK_ACCOUNT: 'add-verified-bank-account', HOME: '', SETTINGS: 'settings', SETTINGS_PROFILE: 'settings/profile', diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 7ea86bbe9906..7f17391134e8 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -49,7 +49,6 @@ import { NewChatModalStackNavigator, SettingsModalStackNavigator, EnablePaymentsStackNavigator, - BusinessBankAccountModalStackNavigator, AddPersonalBankAccountModalStackNavigator, ReimbursementAccountModalStackNavigator, } from './ModalStackNavigators'; @@ -283,12 +282,6 @@ class AuthScreens extends React.Component { component={ReimbursementAccountModalStackNavigator} listeners={modalScreenListeners} /> - ); } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 9e67d577745e..61ae2a78c7f4 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -159,13 +159,6 @@ const ReimbursementAccountModalStackNavigator = createModalStackNavigator([{ name: 'ReimbursementAccount_Root', }]); -const BusinessBankAccountModalStackNavigator = createModalStackNavigator([ - { - Component: BusinessBankAccountNewPage, - name: 'BusinessBankAccount_New', - }, -]); - export { IOUBillStackNavigator, IOURequestModalStackNavigator, @@ -177,7 +170,6 @@ export { NewChatModalStackNavigator, SettingsModalStackNavigator, EnablePaymentsStackNavigator, - BusinessBankAccountModalStackNavigator, AddPersonalBankAccountModalStackNavigator, ReimbursementAccountModalStackNavigator, }; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index ebd73267b554..ae4518b4edc1 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -109,11 +109,6 @@ export default { AddPersonalBankAccount_Root: ROUTES.ADD_PERSONAL_BANK_ACCOUNT, }, }, - BusinessBankAccount: { - screens: { - BusinessBankAccount_New: ROUTES.BANK_ACCOUNT_NEW, - }, - }, EnablePayments: { screens: { EnablePayments_Root: ROUTES.ENABLE_PAYMENTS, @@ -121,7 +116,7 @@ export default { }, ReimbursementAccount: { screens: { - ReimbursementAccount_Root: 'reimbursement-account', + ReimbursementAccount_Root: ROUTES.ADD_VERIFIED_BANK_ACCOUNT, }, }, }, diff --git a/src/pages/BusinessBankAccount/NewPage.js b/src/pages/BusinessBankAccount/NewPage.js index 7e0bf0fd27c0..94303d938f54 100644 --- a/src/pages/BusinessBankAccount/NewPage.js +++ b/src/pages/BusinessBankAccount/NewPage.js @@ -39,7 +39,7 @@ class BusinessBankAccountNewPage extends React.Component { this.addManualAccount = this.addManualAccount.bind(this); this.state = { - // One of CONST.BANK_ACCOUNT.ADD_METHOD + // One of CONST.BANK_ACCOUNT.SETUP_TYPE bankAccountAddMethod: undefined, hasAcceptedTerms: false, routingNumber: '', @@ -94,7 +94,7 @@ class BusinessBankAccountNewPage extends React.Component { icon={Bank} title={this.props.translate('bankAccount.logIntoYourBank')} onPress={() => { - this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.ADD_METHOD.PLAID}); + this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}); }} shouldShowRightIcon /> @@ -102,7 +102,7 @@ class BusinessBankAccountNewPage extends React.Component { icon={Paycheck} title={this.props.translate('bankAccount.connectManually')} onPress={() => { - this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.ADD_METHOD.MANUAL}); + this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL}); }} shouldShowRightIcon /> @@ -125,7 +125,7 @@ class BusinessBankAccountNewPage extends React.Component { )} - {this.state.bankAccountAddMethod === CONST.BANK_ACCOUNT.ADD_METHOD.PLAID && ( + {this.state.bankAccountAddMethod === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID && ( { @@ -136,7 +136,7 @@ class BusinessBankAccountNewPage extends React.Component { }} /> )} - {this.state.bankAccountAddMethod === CONST.BANK_ACCOUNT.ADD_METHOD.MANUAL && ( + {this.state.bankAccountAddMethod === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL && ( <> From e966390d81d4592fbe5568796e9f84b2d54eca3d Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 11 Jun 2021 09:35:39 -1000 Subject: [PATCH 09/24] reorganize things a bit and fix incorrect API usage --- .../AppNavigator/ModalStackNavigators.js | 3 +- src/libs/actions/BankAccounts.js | 10 +- src/pages/BusinessBankAccount/NewPage.js | 202 ------------------ .../ReimbursementAccount/ACHContractStep.js | 5 + .../ReimbursementAccount/BankAccountStep.js | 178 +++++++++++++++ src/pages/ReimbursementAccount/CompanyStep.js | 5 + .../ReimbursementAccountPage.js | 46 +++- .../ReimbursementAccount/RequestorStep.js | 5 + .../ReimbursementAccount/ValidationStep.js | 5 + 9 files changed, 245 insertions(+), 214 deletions(-) delete mode 100644 src/pages/BusinessBankAccount/NewPage.js create mode 100644 src/pages/ReimbursementAccount/ACHContractStep.js create mode 100644 src/pages/ReimbursementAccount/BankAccountStep.js create mode 100644 src/pages/ReimbursementAccount/CompanyStep.js rename src/pages/{ => ReimbursementAccount}/ReimbursementAccountPage.js (67%) create mode 100644 src/pages/ReimbursementAccount/RequestorStep.js create mode 100644 src/pages/ReimbursementAccount/ValidationStep.js diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 61ae2a78c7f4..effa1e2decda 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -20,9 +20,8 @@ import SettingsAddSecondaryLoginPage from '../../../pages/settings/AddSecondaryL import IOUCurrencySelection from '../../../pages/iou/IOUCurrencySelection'; import ReportParticipantsPage from '../../../pages/ReportParticipantsPage'; import EnablePaymentsPage from '../../../pages/EnablePayments'; -import BusinessBankAccountNewPage from '../../../pages/BusinessBankAccount/NewPage'; import AddPersonalBankAccountPage from '../../../pages/AddPersonalBankAccountPage'; -import ReimbursementAccountPage from '../../../pages/ReimbursementAccountPage'; +import ReimbursementAccountPage from '../../../pages/ReimbursementAccount/ReimbursementAccountPage'; const defaultSubRouteOptions = { cardStyle: styles.navigationScreenCardStyle, diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index f80ab4677706..8674b34f6207 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -275,8 +275,8 @@ function fetchUserWallet() { let previousACHData = {}; Onyx.connect({ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - callback: (val = {}) => { - previousACHData = val.achData || {}; + callback: (val) => { + previousACHData = lodashGet(val, 'achData', {}); }, }); @@ -555,9 +555,9 @@ function setupWithdrawalAccount(data) { if (currentStep === 'ACHContractStep') { // Get an up-to-date bank account list so that we can allow the user to validate their newly // generated bank account - return API.get({returnValueList: 'bankAccountList'}) - .done((json) => { - const bankAccountJSON = _.findWhere(json.bankAccountList, { + return API.Get({returnValueList: 'bankAccountList'}) + .then((bankAccountListResponse) => { + const bankAccountJSON = _.findWhere(bankAccountListResponse.bankAccountList, { bankAccountID: previousACHData.bankAccountID, }); const bankAccount = new BankAccount(bankAccountJSON); diff --git a/src/pages/BusinessBankAccount/NewPage.js b/src/pages/BusinessBankAccount/NewPage.js deleted file mode 100644 index 94303d938f54..000000000000 --- a/src/pages/BusinessBankAccount/NewPage.js +++ /dev/null @@ -1,202 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import PropTypes from 'prop-types'; -import {View, Image} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import ScreenWrapper from '../../components/ScreenWrapper'; -import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; -import MenuItem from '../../components/MenuItem'; -import {Paycheck, Bank, Lock} from '../../components/Icon/Expensicons'; -import styles from '../../styles/styles'; -import TextLink from '../../components/TextLink'; -import Button from '../../components/Button'; -import Icon from '../../components/Icon'; -import colors from '../../styles/colors'; -import Navigation from '../../libs/Navigation/Navigation'; -import Permissions from '../../libs/Permissions'; -import CONST from '../../CONST'; -import TextInputWithLabel from '../../components/TextInputWithLabel'; -import AddPlaidBankAccount from '../../components/AddPlaidBankAccount'; -import ONYXKEYS from '../../ONYXKEYS'; -import CheckboxWithLabel from '../../components/CheckboxWithLabel'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import compose from '../../libs/compose'; -import exampleCheckImage from '../../../assets/images/example-check-image.png'; -import Text from '../../components/Text'; - -const propTypes = { - /** List of betas */ - betas: PropTypes.arrayOf(PropTypes.string).isRequired, - - ...withLocalizePropTypes, -}; - -class BusinessBankAccountNewPage extends React.Component { - constructor(props) { - super(props); - - this.toggleTerms = this.toggleTerms.bind(this); - this.addManualAccount = this.addManualAccount.bind(this); - - this.state = { - // One of CONST.BANK_ACCOUNT.SETUP_TYPE - bankAccountAddMethod: undefined, - hasAcceptedTerms: false, - routingNumber: '', - accountNumber: '', - }; - } - - toggleTerms() { - this.setState(prevState => ({ - hasAcceptedTerms: !prevState.hasAcceptedTerms, - })); - } - - /** - * @returns {Boolean} - */ - canSubmitManually() { - return this.state.hasAcceptedTerms - - // These are taken from BankCountry.js in Web-Secure - && CONST.BANK_ACCOUNT.REGEX.IBAN.test(this.state.accountNumber.trim()) - && CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(this.state.routingNumber.trim()); - } - - addManualAccount() { - // @TODO call API to add account manually - } - - render() { - if (!Permissions.canUseFreePlan(this.props.betas)) { - console.debug('Not showing new bank account page because user is not on free plan beta'); - Navigation.dismissModal(); - return null; - } - - return ( - - - this.setState({bankAccountAddMethod: undefined})} - shouldShowBackButton={!_.isUndefined(this.state.bankAccountAddMethod)} - /> - {!this.state.bankAccountAddMethod && ( - <> - - - {this.props.translate('bankAccount.toGetStarted')} - - { - this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}); - }} - shouldShowRightIcon - /> - { - this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL}); - }} - shouldShowRightIcon - /> - - - {this.props.translate('common.privacy')} - - - - {this.props.translate('bankAccount.yourDataIsSecure')} - - - - - - - - - )} - {this.state.bankAccountAddMethod === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID && ( - { - console.debug(args); - }} - onExitPlaid={() => { - this.setState({bankAccountAddMethod: undefined}); - }} - /> - )} - {this.state.bankAccountAddMethod === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL && ( - <> - - - {this.props.translate('bankAccount.checkHelpLine')} - - - this.setState({routingNumber})} - /> - this.setState({accountNumber})} - /> - ( - - - {this.props.translate('bankAccount.iAcceptThe')} - - - {`Expensify ${this.props.translate('common.termsOfService')}`} - - - )} - /> - -