diff --git a/src/CONST.js b/src/CONST.js
index 44d5b29519ed..ef7f2109060e 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -6,8 +6,41 @@ const CONST = {
IOS: 'https://apps.apple.com/us/app/expensify-cash/id1530278510',
DESKTOP: 'https://expensify.cash/Expensify.cash.dmg',
},
+ SMS: {
+ DOMAIN: '@expensify.sms',
+ },
BANK_ACCOUNT: {
- ADD_METHOD: {
+ STEP: {
+ // In the order they appear in the VBA flow
+ 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',
+ },
+ FIELDS_TYPE: {
+ LOCAL: 'local',
+ },
+ ONFIDO_RESPONSE: {
+ SDK_TOKEN: 'apiResult.sdkToken',
+ PASS: 'pass',
+ },
+ QUESTIONS: {
+ QUESTION: 'apiResult.questions.question',
+ DIFFERENTIATOR_QUESTION: 'apiResult.differentiator-question',
+ },
+ SETUP_TYPE: {
MANUAL: 'manual',
PLAID: 'plaid',
},
@@ -30,6 +63,12 @@ const CONST = {
PRESSED: 'pressed',
COMPLETE: 'complete',
},
+ COUNTRY: {
+ US: 'US',
+ MX: 'MX',
+ AU: 'AU',
+ CA: 'CA',
+ },
PLATFORM: {
IOS: 'ios',
ANDROID: 'android',
@@ -120,6 +159,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..f32362de4b68 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -98,4 +98,7 @@ export default {
// Object containing Wallet terms step state
WALLET_TERMS: 'walletTerms',
+
+ // Stores information about the active reimbursement account being set up
+ REIMBURSEMENT_ACCOUNT: 'reimbursementAccount',
};
diff --git a/src/ROUTES.js b/src/ROUTES.js
index b46703870e4a..8af98287e5d8 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/languages/en.js b/src/languages/en.js
index 14029b389eeb..fea831a19833 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -273,6 +273,8 @@ export default {
plaidBodyCopy: 'Give your employees an easier way to pay - and get paid back - for company expenses.',
checkHelpLine: 'Your routing number and account number can be found on a check for the account.',
iAcceptThe: 'I accept the ',
+ hasPhoneLoginError: 'To add a verified bank account please ensure your primary login is a valid email and try again. You can add your phone number as a secondary login.',
+ hasBeenThrottledError: ({fromNow}) => `For security reasons, we're taking a break from bank account setup so you can double-check your company information. Please try again ${fromNow}. Sorry!`,
},
addPersonalBankAccountPage: {
enterPassword: 'Enter password',
diff --git a/src/languages/es.js b/src/languages/es.js
index a49bb131d0ad..00e53087b9d2 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -265,6 +265,8 @@ export default {
plaidBodyCopy: 'Ofrezca a sus empleados una forma más sencilla de pagar - y recuperar - los gastos de la empresa.',
checkHelpLine: 'Su número de ruta y número de cuenta se pueden encontrar en un cheque para la cuenta.',
iAcceptThe: 'Acepto los ',
+ hasPhoneLoginError: 'Para agregar una cuenta bancaria verificada, asegúrese de que su inicio de sesión principal sea un correo electrónico válido y vuelva a intentarlo. Puede agregar su número de teléfono como inicio de sesión secundario.',
+ hasBeenThrottledError: ({fromNow}) => `Por razones de seguridad, nos tomamos un descanso de la configuración de la cuenta bancaria para que pueda verificar la información de su empresa. Inténtalo de nuevo ${fromNow}. ¡Lo siento!`,
},
addPersonalBankAccountPage: {
enterPassword: 'Escribe una contraseña',
diff --git a/src/libs/API.js b/src/libs/API.js
index 72acc4efe6f8..391c02db97ac 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/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index 58449c765160..349311267c6c 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -49,8 +49,8 @@ import {
NewChatModalStackNavigator,
SettingsModalStackNavigator,
EnablePaymentsStackNavigator,
- BusinessBankAccountModalStackNavigator,
AddPersonalBankAccountModalStackNavigator,
+ ReimbursementAccountModalStackNavigator,
NewWorkspaceStackNavigator,
} from './ModalStackNavigators';
import SCREENS from '../../../SCREENS';
@@ -281,11 +281,12 @@ class AuthScreens extends React.Component {
name="NewWorkspace"
options={modalScreenOptions}
component={NewWorkspaceStackNavigator}
+ listeners={modalScreenListeners}
/>
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index 238629c640f6..90c941ec2f1f 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -20,8 +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/ReimbursementAccount/ReimbursementAccountPage';
import NewWorkspacePage from '../../../pages/workspace/NewWorkspacePage';
const defaultSubRouteOptions = {
@@ -154,18 +154,16 @@ const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator([{
name: 'AddPersonalBankAccount_Root',
}]);
+const ReimbursementAccountModalStackNavigator = createModalStackNavigator([{
+ Component: ReimbursementAccountPage,
+ name: 'ReimbursementAccount_Root',
+}]);
+
const NewWorkspaceStackNavigator = createModalStackNavigator([{
Component: NewWorkspacePage,
name: 'NewWorkspace_Root',
}]);
-const BusinessBankAccountModalStackNavigator = createModalStackNavigator([
- {
- Component: BusinessBankAccountNewPage,
- name: 'BusinessBankAccount_New',
- },
-]);
-
export {
IOUBillStackNavigator,
IOURequestModalStackNavigator,
@@ -177,7 +175,7 @@ export {
NewChatModalStackNavigator,
SettingsModalStackNavigator,
EnablePaymentsStackNavigator,
- BusinessBankAccountModalStackNavigator,
AddPersonalBankAccountModalStackNavigator,
+ ReimbursementAccountModalStackNavigator,
NewWorkspaceStackNavigator,
};
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index 58ca48d1cae9..e18b0b75ae1c 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -109,14 +109,14 @@ export default {
AddPersonalBankAccount_Root: ROUTES.ADD_PERSONAL_BANK_ACCOUNT,
},
},
- BusinessBankAccount: {
+ EnablePayments: {
screens: {
- BusinessBankAccount_New: ROUTES.BANK_ACCOUNT_NEW,
+ EnablePayments_Root: ROUTES.ENABLE_PAYMENTS,
},
},
- EnablePayments: {
+ ReimbursementAccount: {
screens: {
- EnablePayments_Root: ROUTES.ENABLE_PAYMENTS,
+ ReimbursementAccount_Root: ROUTES.ADD_VERIFIED_BANK_ACCOUNT,
},
},
NewWorkspace: {
diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js
index 3312d0c0723c..fde8be7c0c24 100644
--- a/src/libs/actions/BankAccounts.js
+++ b/src/libs/actions/BankAccounts.js
@@ -1,10 +1,14 @@
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';
+import promiseAllSettled from '../promiseAllSettled';
+import Growl from '../Growl';
/**
* Gets the Plaid Link token used to initialize the Plaid SDK
@@ -76,7 +80,7 @@ function clearPlaidBankAccountsAndToken() {
* @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
));
@@ -270,12 +274,359 @@ function fetchUserWallet() {
});
}
+let previousACHData = {};
+Onyx.connect({
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ callback: (val) => {
+ previousACHData = lodashGet(val, 'achData', {});
+ },
+});
+
+/**
+ * Navigate to a specific step in the VBA flow
+ *
+ * @param {String} stepID
+ * @param {Object} achData
+ */
+function goToWithdrawalAccountSetupStep(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 === CONST.BANK_ACCOUNT.STEP.REQUESTOR) {
+ delete newACHData.questions;
+ delete newACHData.answers;
+ if (lodashHas(achData, CONST.BANK_ACCOUNT.VERIFICATIONS.EXTERNAL_API_RESPONSES)) {
+ delete newACHData.verifications.externalApiResponses.requestorIdentityID;
+ delete newACHData.verifications.externalApiResponses.requestorIdentityKBA;
+ }
+ }
+
+ // When going back to the BankAccountStep from the Company Step, show the manual form instead of Plaid
+ if (newACHData.currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && stepID === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT) {
+ newACHData.subStep = 'manual';
+ }
+
+ 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.REIMBURSEMENT_ACCOUNT, {loading: true});
+ promiseAllSettled([
+ API.Get({
+ returnValueList: 'nameValuePairs',
+ name: CONST.NVP.FREE_PLAN_BANK_ACCOUNT_ID,
+ }),
+ API.Get({
+ returnValueList: 'nameValuePairs',
+ name: 'expensify_migration_2020_04_28_RunKycVerifications',
+ }),
+ API.Get({
+ returnValueList: 'nameValuePairs',
+ name: CONST.NVP.ACH_DATA_THROTTLED,
+ }),
+ API.Get({returnValueList: 'bankAccountList'}),
+ ])
+ .then(([
+ freePlanBankAccountIDResponse,
+ kycVerificationsMigrationResponse,
+ achDataThrottledResponse,
+ bankAccountListResponse,
+ ]) => {
+ const bankAccountID = lodashGet(freePlanBankAccountIDResponse, [
+ 'value', 'nameValuePairs', CONST.NVP.FREE_PLAN_BANK_ACCOUNT_ID,
+ ], '');
+ const kycVerificationsMigration = lodashGet(kycVerificationsMigrationResponse, [
+ 'value', 'nameValuePairs', 'expensify_migration_2020_04_28_RunKycVerifications',
+ ], '');
+ const throttledDate = lodashGet(achDataThrottledResponse, [
+ 'value', 'nameValuePairs', CONST.NVP.ACH_DATA_THROTTLED,
+ ], '');
+ const bankAccountJSON = _.find(
+ lodashGet(bankAccountListResponse, ['value', 'bankAccountList'], []), account => (
+ account.bankAccountID === bankAccountID
+ ),
+ );
+ const bankAccount = bankAccountJSON ? new BankAccount(bankAccountJSON) : null;
+
+ // Next we'll build the achData and save it to Onyx
+ // If the user is already setting up a bank account we will continue the flow for them
+ let currentStep = previousACHData.currentStep;
+ const achData = bankAccount ? bankAccount.toACHData() : {};
+ achData.useOnfido = true;
+ achData.policyID = '';
+ 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 === CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT && achData.useOnfido) {
+ const onfidoResponse = lodashGet(achData, 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
+ ) {
+ currentStep = CONST.BANK_ACCOUNT.STEP.REQUESTOR;
+ }
+ }
+
+ // 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()
+ ? CONST.BANK_ACCOUNT.STEP.VALIDATION
+ : CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT;
+
+ // @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
+ ? CONST.BANK_ACCOUNT.STEP.VALIDATION : CONST.BANK_ACCOUNT.STEP.COMPANY;
+ achData.bankAccountInReview = hasTriedToUpgrade;
+ } else {
+ // In Expensify.cash we do not show a specific view for the EnableStep since we will enable the
+ // Expensify card automatically. However, we will still handle that step and show the Validate
+ // view.
+ currentStep = CONST.BANK_ACCOUNT.STEP.ENABLE;
+ }
+ }
+ }
+
+ // If at this point we still don't have a current step, default to the BankAccountStep
+ if (!currentStep) {
+ currentStep = CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT;
+ }
+
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {throttledDate});
+ goToWithdrawalAccountSetupStep(currentStep, achData);
+ })
+ .finally(() => {
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false});
+ });
+}
+
+const WITHDRAWAL_ACCOUNT_STEPS = [
+ {
+ id: CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT,
+ title: 'Bank Account',
+ },
+ {
+ id: CONST.BANK_ACCOUNT.STEP.COMPANY,
+ title: 'Company Information',
+ },
+ {
+ id: CONST.BANK_ACCOUNT.STEP.REQUESTOR,
+ title: 'Requestor Information',
+ },
+ {
+ id: CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT,
+ title: 'Beneficial Owners',
+ },
+ {
+ id: CONST.BANK_ACCOUNT.STEP.VALIDATION,
+ title: 'Validate',
+ },
+ {
+ id: CONST.BANK_ACCOUNT.STEP.ENABLE,
+ title: 'Enable',
+ },
+];
+
+/**
+ * Get step position in the array
+ * @private
+ * @param {String} stepID
+ * @return {Number}
+ */
+function getIndexByStepID(stepID) {
+ return _.findIndex(WITHDRAWAL_ACCOUNT_STEPS, step => step.id === stepID);
+}
+
+/**
+ * Get next step ID
+ * @return {String}
+ */
+function getNextStepID() {
+ const nextStepIndex = Math.min(
+ getIndexByStepID(previousACHData.currentStep) + 1,
+ WITHDRAWAL_ACCOUNT_STEPS.length - 1,
+ );
+ return lodashGet(WITHDRAWAL_ACCOUNT_STEPS, [nextStepIndex, 'id'], CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT);
+}
+
+/**
+ * @private
+ * @param {Number} bankAccountID
+ */
+function setFreePlanVerifiedBankAccountID(bankAccountID) {
+ API.SetNameValuePair({name: CONST.NVP.FREE_PLAN_BANK_ACCOUNT_ID, value: bankAccountID});
+}
+
+/**
+ * Create or update the bank account in db with the updated data.
+ *
+ * @param {Object} [data]
+ */
+function setupWithdrawalAccount(data) {
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: true});
+
+ previousACHData = {...previousACHData, ...data};
+ if (data && !_.isUndefined(data.isSavings)) {
+ previousACHData.isSavings = Boolean(data.isSavings);
+ }
+ if (!previousACHData.setupType) {
+ previousACHData.setupType = previousACHData.plaidAccountID
+ ? CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID
+ : CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL;
+ }
+
+ let nextStep = previousACHData.currentStep;
+
+ // If we are setting up a Plaid account replace the accountNumber with the unmasked number
+ if (data.plaidAccountID) {
+ const unmaskedAccount = _.find(plaidBankAccounts, bankAccount => (
+ bankAccount.plaidAccountID === data.plaidAccountID
+ ));
+ previousACHData.accountNumber = unmaskedAccount.accountNumber;
+ }
+
+ API.BankAccount_SetupWithdrawal(previousACHData)
+ .then((response) => {
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false});
+
+ const currentStep = previousACHData.currentStep;
+ let achData = lodashGet(response, 'achData', {});
+ let error = lodashGet(achData, CONST.BANK_ACCOUNT.VERIFICATIONS.ERROR_MESSAGE);
+
+ 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.
+ if (achData.bankAccountID) {
+ 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 === CONST.BANK_ACCOUNT.STEP.REQUESTOR) {
+ const requestorResponse = lodashGet(
+ achData,
+ CONST.BANK_ACCOUNT.VERIFICATIONS.REQUESTOR_IDENTITY_ID,
+ );
+ if (previousACHData.useOnfido) {
+ const onfidoResponse = lodashGet(
+ achData,
+ CONST.BANK_ACCOUNT.VERIFICATIONS.REQUESTOR_IDENTITY_ONFIDO,
+ );
+ const sdkToken = lodashGet(onfidoResponse, CONST.BANK_ACCOUNT.ONFIDO_RESPONSE.SDK_TOKEN);
+ if (sdkToken && !previousACHData.isOnfidoSetupComplete
+ && onfidoResponse.status !== CONST.BANK_ACCOUNT.ONFIDO_RESPONSE.PASS
+ ) {
+ // Requestor Step still needs to run Onfido
+ achData.sdkToken = sdkToken;
+ goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR, achData);
+ return;
+ }
+ } else if (requestorResponse) {
+ // Don't go to next step if Requestor Step needs to ask some questions
+ let questions = lodashGet(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)) {
+ achData.questions = questions;
+ goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.REQUESTOR, achData);
+ return;
+ }
+ }
+ }
+
+ 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
+ API.Get({returnValueList: 'bankAccountList'})
+ .then((bankAccountListResponse) => {
+ const bankAccountJSON = _.findWhere(bankAccountListResponse.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;
+
+ goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.VALIDATION, achData);
+ });
+ return;
+ }
+
+ if ((currentStep === CONST.BANK_ACCOUNT.STEP.VALIDATION && previousACHData.bankAccountInReview)
+ || currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE
+ ) {
+ // Setup done!
+ } else {
+ nextStep = getNextStepID();
+ }
+ } else {
+ if (response.jsonCode === 666) {
+ error = response.message;
+ }
+ if (lodashGet(achData, CONST.BANK_ACCOUNT.VERIFICATIONS.THROTTLED)) {
+ achData.disableFields = true;
+ }
+ }
+
+ // Go to next step
+ goToWithdrawalAccountSetupStep(nextStep, achData);
+
+ if (error) {
+ Growl.show(`Error setting up account: ${error}`, CONST.GROWL.ERROR, 5000);
+ }
+ });
+}
+
export {
fetchPlaidLinkToken,
- addPlaidBankAccount,
+ addPersonalBankAccount,
getPlaidBankAccounts,
clearPlaidBankAccountsAndToken,
fetchOnfidoToken,
activateWallet,
fetchUserWallet,
+ fetchFreePlanVerifiedBankAccount,
+ setupWithdrawalAccount,
+ goToWithdrawalAccountSetupStep,
};
diff --git a/src/libs/models/BankAccount.js b/src/libs/models/BankAccount.js
new file mode 100644
index 000000000000..c2a68747e85b
--- /dev/null
+++ b/src/libs/models/BankAccount.js
@@ -0,0 +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 that have 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;
diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js
index f82546dc1f73..e6e7f22e63e0 100644
--- a/src/pages/AddPersonalBankAccountPage.js
+++ b/src/pages/AddPersonalBankAccountPage.js
@@ -3,7 +3,7 @@ import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
import ScreenWrapper from '../components/ScreenWrapper';
import Navigation from '../libs/Navigation/Navigation';
import {
- addPlaidBankAccount,
+ addPersonalBankAccount,
} from '../libs/actions/BankAccounts';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import AddPlaidBankAccount from '../components/AddPlaidBankAccount';
@@ -20,7 +20,7 @@ const AddPersonalBankAccountPage = props => (
/>
{
- addPlaidBankAccount(account, password, plaidLinkToken);
+ addPersonalBankAccount(account, password, plaidLinkToken);
}}
onExitPlaid={Navigation.dismissModal}
/>
diff --git a/src/pages/BusinessBankAccount/NewPage.js b/src/pages/BusinessBankAccount/NewPage.js
deleted file mode 100644
index 22fad1997dbb..000000000000
--- a/src/pages/BusinessBankAccount/NewPage.js
+++ /dev/null
@@ -1,201 +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.ADD_METHOD
- 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');
- return ;
- }
-
- return (
-
-
- this.setState({bankAccountAddMethod: undefined})}
- shouldShowBackButton={!_.isUndefined(this.state.bankAccountAddMethod)}
- />
- {!this.state.bankAccountAddMethod && (
- <>
-
-
- {this.props.translate('bankAccount.toGetStarted')}
-
-
- >
- )}
- {this.state.bankAccountAddMethod === CONST.BANK_ACCOUNT.ADD_METHOD.PLAID && (
- {
- console.debug(args);
- }}
- onExitPlaid={() => {
- this.setState({bankAccountAddMethod: undefined});
- }}
- />
- )}
- {this.state.bankAccountAddMethod === CONST.BANK_ACCOUNT.ADD_METHOD.MANUAL && (
- <>
-
-
- {this.props.translate('bankAccount.checkHelpLine')}
-
-
- this.setState({routingNumber})}
- />
- this.setState({accountNumber})}
- />
- (
-
-
- {this.props.translate('bankAccount.iAcceptThe')}
-
-
- {`Expensify ${this.props.translate('common.termsOfService')}`}
-
-
- )}
- />
-
-
- >
- )}
-
-
- );
- }
-}
-
-BusinessBankAccountNewPage.propTypes = propTypes;
-
-export default compose(
- withOnyx({
- betas: {
- key: ONYXKEYS.BETAS,
- },
- }),
- withLocalize,
-)(BusinessBankAccountNewPage);
diff --git a/src/pages/ReimbursementAccount/ACHContractStep.js b/src/pages/ReimbursementAccount/ACHContractStep.js
new file mode 100644
index 000000000000..2434bd9e9f3e
--- /dev/null
+++ b/src/pages/ReimbursementAccount/ACHContractStep.js
@@ -0,0 +1,5 @@
+import React from 'react';
+import {View} from 'react-native';
+
+const ACHContractStep = () => ;
+export default ACHContractStep;
diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js
new file mode 100644
index 000000000000..edfaa3bf3987
--- /dev/null
+++ b/src/pages/ReimbursementAccount/BankAccountStep.js
@@ -0,0 +1,223 @@
+import _ from 'underscore';
+import React from 'react';
+import {View, Image} from 'react-native';
+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 CONST from '../../CONST';
+import TextInputWithLabel from '../../components/TextInputWithLabel';
+import AddPlaidBankAccount from '../../components/AddPlaidBankAccount';
+import CheckboxWithLabel from '../../components/CheckboxWithLabel';
+import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import exampleCheckImage from '../../../assets/images/example-check-image.png';
+import Text from '../../components/Text';
+import {setupWithdrawalAccount} from '../../libs/actions/BankAccounts';
+
+const propTypes = {
+ ...withLocalizePropTypes,
+};
+
+class BankAccountStep extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.toggleTerms = this.toggleTerms.bind(this);
+ this.addManualAccount = this.addManualAccount.bind(this);
+ this.addPlaidAccount = this.addPlaidAccount.bind(this);
+
+ this.state = {
+ // One of CONST.BANK_ACCOUNT.SETUP_TYPE
+ bankAccountAddMethod: undefined,
+ hasAcceptedTerms: true,
+ 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() {
+ setupWithdrawalAccount({
+ acceptTerms: this.state.hasAcceptedTerms,
+ accountNumber: this.state.accountNumber,
+ routingNumber: this.state.routingNumber,
+ setupType: CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL,
+
+ // Note: These are hardcoded as we're not supporting AU bank accounts for the free plan
+ country: CONST.COUNTRY.US,
+ currency: CONST.CURRENCY.USD,
+ fieldsType: CONST.BANK_ACCOUNT.FIELDS_TYPE.LOCAL,
+ });
+ }
+
+ /**
+ * @param {Object} params
+ * @param {String} params.password
+ * @param {Object} params.account
+ * @param {String} params.account.bankName
+ * @param {Boolean} params.account.isSavings
+ * @param {String} params.account.addressName
+ * @param {String} params.account.ownershipType
+ * @param {String} params.account.accountNumber
+ * @param {String} params.account.routingNumber
+ * @param {String} params.account.plaidAccountID
+ */
+ addPlaidAccount(params) {
+ setupWithdrawalAccount({
+ acceptTerms: true,
+ setupType: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID,
+
+ // Params passed via the Plaid callback when an account is selected
+ password: params.password,
+ plaidAccessToken: params.plaidLinkToken,
+ accountNumber: params.account.accountNumber,
+ routingNumber: params.account.routingNumber,
+ plaidAccountID: params.account.plaidAccountID,
+ ownershipType: params.account.ownershipType,
+ isSavings: params.account.isSavings,
+ bankName: params.account.bankName,
+ addressName: params.account.addressName,
+
+ // Note: These are hardcoded as we're not supporting AU bank accounts for the free plan
+ country: CONST.COUNTRY.US,
+ currency: CONST.CURRENCY.USD,
+ fieldsType: CONST.BANK_ACCOUNT.FIELDS_TYPE.LOCAL,
+ });
+ }
+
+ render() {
+ 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 && (
+ {
+ 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')}`}
+
+
+ )}
+ />
+
+
+ >
+ )}
+
+ );
+ }
+}
+
+BankAccountStep.propTypes = propTypes;
+
+export default withLocalize(BankAccountStep);
diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js
new file mode 100644
index 000000000000..8b8dc52f38f1
--- /dev/null
+++ b/src/pages/ReimbursementAccount/CompanyStep.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import {View} from 'react-native';
+import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
+import CONST from '../../CONST';
+import {goToWithdrawalAccountSetupStep} from '../../libs/actions/BankAccounts';
+import Navigation from '../../libs/Navigation/Navigation';
+
+const CompanyStep = () => (
+
+ goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT)}
+ onCloseButtonPress={Navigation.dismissModal}
+ />
+
+);
+
+export default CompanyStep;
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
new file mode 100644
index 000000000000..4435525e47fd
--- /dev/null
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
@@ -0,0 +1,144 @@
+import moment from 'moment';
+import lodashGet from 'lodash/get';
+import React from 'react';
+import {withOnyx} from 'react-native-onyx';
+import Str from 'expensify-common/lib/str';
+import {View, Text} from 'react-native';
+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';
+import Permissions from '../../libs/Permissions';
+
+// Steps
+import BankAccountStep from './BankAccountStep';
+import CompanyStep from './CompanyStep';
+import RequestorStep from './RequestorStep';
+import ACHContractStep from './ACHContractStep';
+import ValidationStep from './ValidationStep';
+import Navigation from '../../libs/Navigation/Navigation';
+import CONST from '../../CONST';
+import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import compose from '../../libs/compose';
+import styles from '../../styles/styles';
+
+const propTypes = {
+ /** List of betas */
+ betas: PropTypes.arrayOf(PropTypes.string).isRequired,
+
+ /** ACH data for the withdrawal account actively being set up */
+ reimbursementAccount: PropTypes.shape({
+ /** Whether we are loading the data via the API */
+ loading: PropTypes.bool,
+
+ /** A date that indicates the user has been throttled */
+ throttledDate: PropTypes.string,
+
+ /** Additional data for the account in setup */
+ achData: PropTypes.shape({
+
+ /** Step of the setup flow that we are on. Determines which view is presented. */
+ currentStep: PropTypes.string,
+ }),
+ }),
+
+ /** Current session for the user */
+ session: PropTypes.shape({
+ /** User login */
+ email: PropTypes.string,
+ }).isRequired,
+
+ ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+ reimbursementAccount: {
+ loading: true,
+ },
+};
+
+class ReimbursementAccountPage extends React.Component {
+ componentDidMount() {
+ fetchFreePlanVerifiedBankAccount();
+ }
+
+ 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;
+ }
+
+ if (this.props.reimbursementAccount.loading) {
+ return ;
+ }
+
+ const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, CONST.SMS.DOMAIN);
+
+ if (userHasPhonePrimaryEmail) {
+ return (
+
+ {this.props.translate('bankAccount.hasPhoneLoginError')}
+
+ );
+ }
+
+ const throttledDate = lodashGet(this.props, 'reimbursementAccount.throttledDate');
+ if (throttledDate) {
+ const throttledEnd = moment().add(24, 'hours');
+ if (moment() < throttledEnd) {
+ return (
+
+
+ {this.props.translate('bankAccount.hasBeenThrottledError', {
+ fromNow: throttledEnd.fromNow(),
+ })}
+
+
+ );
+ }
+ }
+
+ // We grab the currentStep from the achData to determine which view to display. The SetupWithdrawalAccount flow
+ // allows us to continue the flow from various points depending on where the user left off.
+ const currentStep = this.props.reimbursementAccount.achData.currentStep;
+ return (
+
+ {currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT && (
+
+ )}
+ {currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && (
+
+ )}
+ {currentStep === CONST.BANK_ACCOUNT.STEP.REQUESTOR && (
+
+ )}
+ {currentStep === CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT && (
+
+ )}
+ {currentStep === CONST.BANK_ACCOUNT.STEP.VALIDATION && (
+
+ )}
+
+ );
+ }
+}
+
+ReimbursementAccountPage.propTypes = propTypes;
+ReimbursementAccountPage.defaultProps = defaultProps;
+
+export default compose(
+ withOnyx({
+ reimbursementAccount: {
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ }),
+ withLocalize,
+)(ReimbursementAccountPage);
diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js
new file mode 100644
index 000000000000..350e3b58471c
--- /dev/null
+++ b/src/pages/ReimbursementAccount/RequestorStep.js
@@ -0,0 +1,5 @@
+import React from 'react';
+import {View} from 'react-native';
+
+const RequestorStep = () => ;
+export default RequestorStep;
diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js
new file mode 100644
index 000000000000..43628ea79677
--- /dev/null
+++ b/src/pages/ReimbursementAccount/ValidationStep.js
@@ -0,0 +1,5 @@
+import React from 'react';
+import {View} from 'react-native';
+
+const ValidationStep = () => ;
+export default ValidationStep;