From 90dbbf1ef0723a855aadd45c368b35028b127b69 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 10:09:11 -1000 Subject: [PATCH 01/17] Disable plaid when throttled. Clean up logic in BankAccountStep. --- src/CONST.js | 7 ++ src/components/AddPlaidBankAccount.js | 9 +- src/libs/actions/BankAccounts.js | 105 +++++++++++------- .../ReimbursementAccount/BankAccountStep.js | 18 +-- .../ReimbursementAccountPage.js | 5 +- 5 files changed, 87 insertions(+), 57 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 8e047d087b5a..3f6fc83e79a4 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -10,6 +10,12 @@ const CONST = { DOMAIN: '@expensify.sms', }, BANK_ACCOUNT: { + PLAID: { + ALLOWED_THROTTLED_COUNT: 2, + ERROR: { + TOO_MANY_ATTEMPTS: 'Too many attempts', + }, + }, STEP: { // In the order they appear in the VBA flow BANK_ACCOUNT: 'BankAccountStep', @@ -170,6 +176,7 @@ const CONST = { TIMEZONE: 'timeZone', FREE_PLAN_BANK_ACCOUNT_ID: 'expensify_freePlanBankAccountID', ACH_DATA_THROTTLED: 'expensify_ACHData_throttled', + BANK_ACCOUNT_GET_THROTTLED: 'private_throttledHistory_BankAccount_Get', }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {error: '', success: '', loading: false}, diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 4339a9c784ef..dc7b1bd51222 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -96,12 +96,9 @@ class AddPlaidBankAccount extends React.Component { render() { const accounts = this.getAccounts(); - const options = _.chain(accounts) - .filter(account => !account.alreadyExists) - .map((account, index) => ({ - value: index, label: `${account.addressName} ${account.accountNumber}`, - })) - .value(); + const options = _.map(accounts, (account, index) => ({ + value: index, label: `${account.addressName} ${account.accountNumber}`, + })); return ( <> diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 3a5fa92ef757..bc81ce8d815d 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -10,6 +10,24 @@ import BankAccount from '../models/BankAccount'; import promiseAllSettled from '../promiseAllSettled'; import Growl from '../Growl'; +/** + * List of bank accounts. This data should not be stored in Onyx since it contains unmasked PANs. + * + * @private + */ +let plaidBankAccounts = []; +let bankName = ''; +let plaidAccessToken = ''; + +/** Reimbursement account actively being set up */ +let reimbursementAccountInSetup = {}; +Onyx.connect({ + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + callback: (val) => { + reimbursementAccountInSetup = lodashGet(val, 'achData', {}); + }, +}); + /** * Gets the Plaid Link token used to initialize the Plaid SDK */ @@ -25,13 +43,31 @@ function fetchPlaidLinkToken() { } /** - * List of bank accounts. This data should not be stored in Onyx since it contains unmasked PANs. + * Navigate to a specific step in the VBA flow * - * @private + * @param {String} stepID + * @param {Object} achData */ -let plaidBankAccounts = []; -let bankName = ''; -let plaidAccessToken = ''; +function goToWithdrawalAccountSetupStep(stepID, achData) { + const newACHData = {...reimbursementAccountInSetup}; + + // 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}}); +} /** * @param {String} publicToken @@ -47,8 +83,19 @@ function getPlaidBankAccounts(publicToken, bank) { bank, }) .then((response) => { + if (response.jsonCode === 666 && response.title === CONST.BANK_ACCOUNT.PLAID.ERROR.TOO_MANY_ATTEMPTS) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isPlaidDisabled: true}); + } + plaidAccessToken = response.plaidAccessToken; - plaidBankAccounts = response.accounts; + + // Filter out any accounts that already exist since they cannot be used again. + plaidBankAccounts = _.filter(response.accounts, account => !account.alreadyExists); + + if (plaidBankAccounts.length === 0) { + Growl.show('Sorry, no bank account is available', CONST.GROWL.ERROR); + } + Onyx.merge(ONYXKEYS.PLAID_BANK_ACCOUNTS, { error: { title: response.title, @@ -274,41 +321,6 @@ function fetchUserWallet() { }); } -let reimbursementAccountInSetup = {}; -Onyx.connect({ - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - callback: (val) => { - reimbursementAccountInSetup = lodashGet(val, 'achData', {}); - }, -}); - -/** - * Navigate to a specific step in the VBA flow - * - * @param {String} stepID - * @param {Object} achData - */ -function goToWithdrawalAccountSetupStep(stepID, achData) { - const newACHData = {...reimbursementAccountInSetup}; - - // 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. */ @@ -330,12 +342,17 @@ function fetchFreePlanVerifiedBankAccount() { name: CONST.NVP.ACH_DATA_THROTTLED, }), API.Get({returnValueList: 'bankAccountList'}), + API.Get({ + returnValueList: 'nameValuePairs', + name: CONST.NVP.BANK_ACCOUNT_GET_THROTTLED, + }), ]) .then(([ freePlanBankAccountIDResponse, kycVerificationsMigrationResponse, achDataThrottledResponse, bankAccountListResponse, + throttledBankAccountGetResponse, ]) => { const bankAccountID = lodashGet(freePlanBankAccountIDResponse, [ 'value', 'nameValuePairs', CONST.NVP.FREE_PLAN_BANK_ACCOUNT_ID, @@ -352,6 +369,10 @@ function fetchFreePlanVerifiedBankAccount() { ), ); const bankAccount = bankAccountJSON ? new BankAccount(bankAccountJSON) : null; + const throttledHistoryCount = lodashGet(throttledBankAccountGetResponse, [ + 'value', 'nameValuePairs', CONST.NVP.BANK_ACCOUNT_GET_THROTTLED, + ], 0); + const isPlaidDisabled = throttledHistoryCount > CONST.BANK_ACCOUNT.PLAID.ALLOWED_THROTTLED_COUNT; // 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 @@ -417,7 +438,7 @@ function fetchFreePlanVerifiedBankAccount() { currentStep = CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT; } - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {throttledDate}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {throttledDate, isPlaidDisabled}); goToWithdrawalAccountSetupStep(currentStep, achData); }) .finally(() => { diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 15674eeee1aa..7e6dfc8443f9 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -123,14 +123,16 @@ class BankAccountStep extends React.Component { {this.props.translate('bankAccount.toGetStarted')} - { - this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}); - }} - shouldShowRightIcon - /> + {!this.props.isPlaidDisabled && ( + { + this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}); + }} + shouldShowRightIcon + /> + )} {currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT && ( - + )} {currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && ( From 5e26bbf0d9bc8faf35c5bc23c18ae6e8d4e58ac3 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 10:30:11 -1000 Subject: [PATCH 02/17] add growls --- src/CONST.js | 3 + src/components/Onfido/index.js | 2 +- src/components/Onfido/index.native.js | 2 +- src/components/TextInputFocusable/index.js | 2 +- src/libs/Growl.js | 14 +++- src/libs/actions/BankAccounts.js | 4 +- src/libs/actions/Policy.js | 4 +- src/libs/actions/Report.js | 2 +- src/pages/ReimbursementAccount/CompanyStep.js | 75 ++++++++++++++++++- src/pages/workspace/WorkspaceInvitePage.js | 3 +- 10 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 3f6fc83e79a4..96c919e78b71 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -6,6 +6,9 @@ const CONST = { IOS: 'https://apps.apple.com/us/app/expensify-cash/id1530278510', DESKTOP: 'https://expensify.cash/Expensify.cash.dmg', }, + DATE: { + MOMENT_FORMAT_STRING: 'YYYY-MM-DD', + }, SMS: { DOMAIN: '@expensify.sms', }, diff --git a/src/components/Onfido/index.js b/src/components/Onfido/index.js index 1a5a64d83d1f..5c1f01d41187 100644 --- a/src/components/Onfido/index.js +++ b/src/components/Onfido/index.js @@ -47,7 +47,7 @@ class Onfido extends React.Component { onComplete: this.props.onSuccess, onError: () => { this.props.onUserExit(); - Growl.show(this.props.translate('onfidoStep.genericError'), CONST.GROWL.ERROR); + Growl.error(this.props.translate('onfidoStep.genericError')); }, onUserExit: this.props.onUserExit, onModalRequestClose: () => {}, diff --git a/src/components/Onfido/index.native.js b/src/components/Onfido/index.native.js index 9f46b56361f0..43970fb0a7fd 100644 --- a/src/components/Onfido/index.native.js +++ b/src/components/Onfido/index.native.js @@ -34,7 +34,7 @@ class Onfido extends React.Component { .catch((error) => { if (error.message === CONST.ONFIDO.ERROR.USER_CANCELLED) { this.props.onUserExit(); - Growl.show(this.props.translate('onfidoStep.genericError'), CONST.GROWL.ERROR); + Growl.error(this.props.translate('onfidoStep.genericError')); } }); } diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js index 48329e4a5530..b611bfdd2f69 100755 --- a/src/components/TextInputFocusable/index.js +++ b/src/components/TextInputFocusable/index.js @@ -230,7 +230,7 @@ class TextInputFocusable extends React.Component { .then(this.props.onPasteFile) .catch(() => { const errorDesc = this.props.translate('textInputFocusable.problemGettingImageYouPasted'); - Growl.show(errorDesc, CONST.GROWL.ERROR); + Growl.error(errorDesc); /* * Since we intercepted the user-triggered paste event to check for attachments, diff --git a/src/libs/Growl.js b/src/libs/Growl.js index 9e20dcbcc2a6..d16038480692 100644 --- a/src/libs/Growl.js +++ b/src/libs/Growl.js @@ -14,5 +14,17 @@ function show(bodyText, type, duration = CONST.GROWL.DURATION) { growlRef.current.show(bodyText, type, duration); } +/** + * Show error growl + * + * @param {String} bodyText + * @param {Number} [duration] + */ +function error(bodyText, duration = CONST.GROWL.DURATION) { + show(bodyText, CONST.GROWL.ERROR, duration); +} -export default {show}; +export default { + show, + error, +}; diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index bc81ce8d815d..dcbdc75a6ef3 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -93,7 +93,7 @@ function getPlaidBankAccounts(publicToken, bank) { plaidBankAccounts = _.filter(response.accounts, account => !account.alreadyExists); if (plaidBankAccounts.length === 0) { - Growl.show('Sorry, no bank account is available', CONST.GROWL.ERROR); + Growl.error('Sorry, no bank account is available'); } Onyx.merge(ONYXKEYS.PLAID_BANK_ACCOUNTS, { @@ -634,7 +634,7 @@ function setupWithdrawalAccount(data) { goToWithdrawalAccountSetupStep(nextStep, achData); if (error) { - Growl.show(`Error setting up account: ${error}`, CONST.GROWL.ERROR, 5000); + Growl.error(`Error setting up account: ${error}`); } }); } diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 15e1f69ccfa0..27d87cd1bbe8 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -141,7 +141,7 @@ function invite(login, welcomeNote, policyID) { errorMessage += ` ${translateLocal('workspace.invite.pleaseEnterValidLogin')}`; } - Growl.show(errorMessage, CONST.GROWL.ERROR, 5000); + Growl.error(errorMessage, 5000); }); } @@ -156,7 +156,7 @@ function create(name) { if (response.jsonCode !== 200) { // Show the user feedback const errorMessage = translateLocal('workspace.new.genericFailureMessage'); - Growl.show(errorMessage, CONST.GROWL.ERROR, 5000); + Growl.error(errorMessage, 5000); return; } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index ce05c41675b7..5f039efd9ab4 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1057,7 +1057,7 @@ function addAction(reportID, text, file) { }) .then((response) => { if (response.jsonCode === 408) { - Growl.show(translateLocal('reportActionCompose.fileUploadFailed'), CONST.GROWL.ERROR); + Growl.error(translateLocal('reportActionCompose.fileUploadFailed')); Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [optimisticReportActionID]: null, }); diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index c8184b0c98bb..58038f1ac3b9 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -17,6 +17,8 @@ import Picker from '../../components/Picker'; import StatePicker from '../../components/StatePicker'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import Growl from '../../libs/Growl'; +import Str from 'expensify-common/lib/str'; +import moment from 'moment'; class CompanyStep extends React.Component { constructor(props) { @@ -42,10 +44,78 @@ class CompanyStep extends React.Component { }; } + /** + * Validating that this is a valid address (PO boxes are not allowed) + * @param {String} value + * @returns {Boolean} + */ + isValidAddress(value) { + const isEmpty = !new RegExp('^.+$').test(value); + if (isEmpty) { + return false; + } + + // eslint-disable-next-line max-len + const isPoBox = new RegExp('\\b[P|p]?(OST|ost)?\\.?\\s*[O|o|0]?(ffice|FFICE)?\\.?\\s*[B|b][O|o|0]?[X|x]?\\.?\\s+[#]?(\\d+)\\b').test(value); + return !isPoBox; + } + + /** + * Validate date fields + * @param {String} date + * @returns {Boolean} true if valid + */ + validateDate(date) { + return moment(date).isValid(); + } + + /** + * @param {String} code + * @returns {Boolean} + */ + validateIndustryCode(code) { + return !/^[0-9]{6}$/.test(code); + } + validate() { // @TODO check more than just the password if (!this.state.password.trim()) { - Growl.show(this.props.translate('common.passwordCannotBeBlank'), CONST.GROWL.ERROR); + Growl.error(this.props.translate('common.passwordCannotBeBlank')); + return false; + } + + if (!this.isValidAddress(this.state.addressStreet)) { + Growl.error('Please enter a valid address street that is not a PO Box'); + return false; + } + + if (!/[0-9]{5}(?:[- ][0-9]{4})?/.test(this.state.addressZipCode)) { + Growl.error('Please enter a valid zip code'); + return false; + } + + if (!Str.isValidURL(this.state.website)) { + Growl.error('Please enter a valid website'); + return false; + } + + if (!/[0-9]{9}/.test(this.state.companyTaxID)) { + Growl.error('Please enter a valid Tax Id Number'); + return false; + } + + if (this.validateDate(this.state.incorporationDate)) { + Growl.error('Please enter a valid incorporation date'); + return false; + } + + if (this.validateIndustryCode(this.state.industryCode)) { + Growl.error('Please enter a valid industry classification code'); + return false; + } + + if (!this.state.hasNoConnectionToCannabis) { + Growl.error('Please confirm company is not on the list of restricted businesses'); return false; } @@ -57,7 +127,8 @@ class CompanyStep extends React.Component { return; } - setupWithdrawalAccount({...this.state}); + const incorporationDate = moment(this.state.incorporationDate).format(CONST.DATE.MOMENT_FORMAT_STRING); + setupWithdrawalAccount({...this.state, incorporationDate}); } render() { diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 7637ee9947ea..e2b7f2f9dbd2 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -17,7 +17,6 @@ import TextLink from '../../components/TextLink'; import getEmailKeyboardType from '../../libs/getEmailKeyboardType'; import themeColors from '../../styles/themes/default'; import Growl from '../../libs/Growl'; -import CONST from '../../CONST'; const propTypes = { ...withLocalizePropTypes, @@ -72,7 +71,7 @@ class WorkspaceInvitePage extends React.Component { */ inviteUser() { if (!Str.isValidEmail(this.state.emailOrPhone) && !Str.isValidPhone(this.state.emailOrPhone)) { - Growl.show(this.props.translate('workspace.invite.pleaseEnterValidLogin'), CONST.GROWL.ERROR, 5000); + Growl.error(this.props.translate('workspace.invite.pleaseEnterValidLogin'), 5000); return; } From b39a3bc9bca27fb97db02ad69521b8b3fd6e5b02 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 10:30:59 -1000 Subject: [PATCH 03/17] fix style --- src/components/TextInputFocusable/index.js | 1 - src/pages/ReimbursementAccount/CompanyStep.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js index b611bfdd2f69..ae979787c020 100755 --- a/src/components/TextInputFocusable/index.js +++ b/src/components/TextInputFocusable/index.js @@ -5,7 +5,6 @@ import _ from 'underscore'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import Growl from '../../libs/Growl'; import themeColors from '../../styles/themes/default'; -import CONST from '../../CONST'; const propTypes = { /** Maximum number of lines in the text input */ diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 58038f1ac3b9..9ff2b98d9204 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -2,6 +2,8 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import React from 'react'; import {View, ScrollView} from 'react-native'; +import Str from 'expensify-common/lib/str'; +import moment from 'moment'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import CONST from '../../CONST'; import {goToWithdrawalAccountSetupStep, setupWithdrawalAccount} from '../../libs/actions/BankAccounts'; @@ -17,8 +19,6 @@ import Picker from '../../components/Picker'; import StatePicker from '../../components/StatePicker'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import Growl from '../../libs/Growl'; -import Str from 'expensify-common/lib/str'; -import moment from 'moment'; class CompanyStep extends React.Component { constructor(props) { From 9597eeaa65a5cd656d0b475809f7b8bf6314c3bd Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 13:08:54 -1000 Subject: [PATCH 04/17] handle missing info in CompanyStep and some server errors --- src/libs/actions/BankAccounts.js | 18 +++++++++++++++++- src/pages/ReimbursementAccount/CompanyStep.js | 8 ++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index dcbdc75a6ef3..e1d718b1c43d 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -625,6 +625,22 @@ function setupWithdrawalAccount(data) { if (response.jsonCode === 666 || response.jsonCode === 404) { error = response.message; } + + if (response.message === '402 Missing routingNumber' + || response.message === '402 Maximum Size Exceeded routingNumber' + ) { + error = 'Please check Routing Number and try again'; + achData.subStep = 'manual'; + } + + if (response.message === '402 Missing incorporationState in additionalData') { + error = 'Please check Incorporation State and try again'; + } + + if (response.message === '402 Missing incorporationType in additionalData') { + error = 'Please check Company Type and try again'; + } + if (lodashGet(achData, CONST.BANK_ACCOUNT.VERIFICATIONS.THROTTLED)) { achData.disableFields = true; } @@ -634,7 +650,7 @@ function setupWithdrawalAccount(data) { goToWithdrawalAccountSetupStep(nextStep, achData); if (error) { - Growl.error(`Error setting up account: ${error}`); + Growl.error(`Error setting up account: ${error}`, 5000); } }); } diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 9ff2b98d9204..63835b0ddf98 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -30,14 +30,14 @@ class CompanyStep extends React.Component { companyName: lodashGet(props, ['achData', 'companyName'], ''), addressStreet: lodashGet(props, ['achData', 'addressStreet'], ''), addressCity: lodashGet(props, ['achData', 'addressCity'], ''), - addressState: lodashGet(props, ['achData', 'addressState'], ''), + addressState: lodashGet(props, ['achData', 'addressState']) || 'AK', addressZipCode: lodashGet(props, ['achData', 'addressZipCode'], ''), companyPhone: lodashGet(props, ['achData', 'companyPhone'], ''), website: lodashGet(props, ['achData', 'website'], ''), companyTaxID: lodashGet(props, ['achData', 'companyTaxID'], ''), incorporationType: lodashGet(props, ['achData', 'incorporationType'], ''), incorporationDate: lodashGet(props, ['achData', 'incorporationDate'], ''), - incorporationState: lodashGet(props, ['achData', 'incorporationState'], ''), + incorporationState: lodashGet(props, ['achData', 'incorporationState']) || 'AK', industryCode: lodashGet(props, ['achData', 'industryCode'], ''), hasNoConnectionToCannabis: lodashGet(props, ['achData', 'hasNoConnectionToCannabis'], false), password: '', @@ -65,7 +65,7 @@ class CompanyStep extends React.Component { * @param {String} date * @returns {Boolean} true if valid */ - validateDate(date) { + isValidDate(date) { return moment(date).isValid(); } @@ -104,7 +104,7 @@ class CompanyStep extends React.Component { return false; } - if (this.validateDate(this.state.incorporationDate)) { + if (!this.isValidDate(this.state.incorporationDate)) { Growl.error('Please enter a valid incorporation date'); return false; } From e4f18ae39f37226ff0fdcba6ec1ec1432e96c94e Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 13:12:16 -1000 Subject: [PATCH 05/17] log out any unhandled error so we can handle while testing in the near future --- src/libs/actions/BankAccounts.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index e1d718b1c43d..fb21b25fd295 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -626,19 +626,19 @@ function setupWithdrawalAccount(data) { error = response.message; } - if (response.message === '402 Missing routingNumber' - || response.message === '402 Maximum Size Exceeded routingNumber' - ) { - error = 'Please check Routing Number and try again'; - achData.subStep = 'manual'; - } - - if (response.message === '402 Missing incorporationState in additionalData') { - error = 'Please check Incorporation State and try again'; - } - - if (response.message === '402 Missing incorporationType in additionalData') { - error = 'Please check Company Type and try again'; + if (response.jsonCode === 402) { + if (response.message === '402 Missing routingNumber' + || response.message === '402 Maximum Size Exceeded routingNumber' + ) { + error = 'Please check Routing Number and try again'; + achData.subStep = 'manual'; + } else if (response.message === '402 Missing incorporationState in additionalData') { + error = 'Please check Incorporation State and try again'; + } else if (response.message === '402 Missing incorporationType in additionalData') { + error = 'Please check Company Type and try again'; + } else { + console.error(response.message); + } } if (lodashGet(achData, CONST.BANK_ACCOUNT.VERIFICATIONS.THROTTLED)) { From 8ecc597a2ba45d4201d311aa1fd1aa6598744c70 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 14:34:22 -1000 Subject: [PATCH 06/17] remove unnecessary text --- src/libs/actions/BankAccounts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index fb21b25fd295..8db588edb473 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -650,7 +650,7 @@ function setupWithdrawalAccount(data) { goToWithdrawalAccountSetupStep(nextStep, achData); if (error) { - Growl.error(`Error setting up account: ${error}`, 5000); + Growl.error(error, 5000); } }); } From e977192e1e2a99b1fb9e007f1c9e943feb954251 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 14:37:35 -1000 Subject: [PATCH 07/17] add errors to const --- src/CONST.js | 6 ++++++ src/libs/actions/BankAccounts.js | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 96c919e78b71..c864d5a1ba71 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -19,6 +19,12 @@ const CONST = { TOO_MANY_ATTEMPTS: 'Too many attempts', }, }, + ERROR: { + MISSING_ROUTING_NUMBER: '402 Missing routingNumber', + MAX_ROUTING_NUMBER: '402 Maximum Size Exceeded routingNumber', + MISSING_INCORPORATION_STATE: '402 Missing incorporationState in additionalData', + MISSING_INCORPORATION_TYPE: '402 Missing incorporationType in additionalData', + }, STEP: { // In the order they appear in the VBA flow BANK_ACCOUNT: 'BankAccountStep', diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 8db588edb473..544186f86f1e 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -627,14 +627,14 @@ function setupWithdrawalAccount(data) { } if (response.jsonCode === 402) { - if (response.message === '402 Missing routingNumber' - || response.message === '402 Maximum Size Exceeded routingNumber' + if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_ROUTING_NUMBER + || response.message === CONST.BANK_ACCOUNT.ERROR.MAX_ROUTING_NUMBER ) { error = 'Please check Routing Number and try again'; achData.subStep = 'manual'; - } else if (response.message === '402 Missing incorporationState in additionalData') { + } else if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_INCORPORATION_STATE) { error = 'Please check Incorporation State and try again'; - } else if (response.message === '402 Missing incorporationType in additionalData') { + } else if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_INCORPORATION_TYPE) { error = 'Please check Company Type and try again'; } else { console.error(response.message); From 38ae3550d0425d314eade5e1e80def8f169447dd Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 14:59:19 -1000 Subject: [PATCH 08/17] translations --- src/languages/en.js | 10 +++++++ src/libs/actions/BankAccounts.js | 3 ++- src/libs/actions/Policy.js | 12 +-------- src/libs/actions/Report.js | 12 +-------- src/libs/translate.js | 27 ++++++++++++++++--- src/pages/ReimbursementAccount/CompanyStep.js | 14 +++++----- 6 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 5d6d6ddbb897..71b404327ecc 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -290,6 +290,16 @@ export default { checkHelpLine: 'Your routing number and account number can be found on a check for the account.', 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!`, + error: { + noBankAccountAvailable: 'Sorry, no bank account is available', + taxID: 'Please enter a valid Tax ID Number', + website: 'Please enter a valid website', + zipCode: 'Please enter a valid zip code', + addressStreet: 'Please enter a valid address street that is not a PO Box', + incorporationDate: 'Please enter a valid incorporation date', + industryCode: 'Please enter a valid industry classification code', + restrictedBusiness: 'Please confirm company is not on the list of restricted businesses', + }, }, addPersonalBankAccountPage: { enterPassword: 'Enter password', diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 544186f86f1e..bd06a9434c29 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -9,6 +9,7 @@ import * as API from '../API'; import BankAccount from '../models/BankAccount'; import promiseAllSettled from '../promiseAllSettled'; import Growl from '../Growl'; +import {translateLocal} from '../translate'; /** * List of bank accounts. This data should not be stored in Onyx since it contains unmasked PANs. @@ -93,7 +94,7 @@ function getPlaidBankAccounts(publicToken, bank) { plaidBankAccounts = _.filter(response.accounts, account => !account.alreadyExists); if (plaidBankAccounts.length === 0) { - Growl.error('Sorry, no bank account is available'); + Growl.error(translateLocal('bankAccount.error.noBankAccountAvailable')); } Onyx.merge(ONYXKEYS.PLAID_BANK_ACCOUNTS, { diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 27d87cd1bbe8..cf33c50d1598 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -7,7 +7,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import {formatPersonalDetails} from './PersonalDetails'; import Growl from '../Growl'; import CONST from '../../CONST'; -import {translate} from '../translate'; +import {translateLocal} from '../translate'; import Navigation from '../Navigation/Navigation'; import ROUTES from '../../ROUTES'; @@ -21,16 +21,6 @@ Onyx.connect({ }, }); -let translateLocal = (phrase, variables) => translate(CONST.DEFAULT_LOCALE, phrase, variables); -Onyx.connect({ - key: ONYXKEYS.PREFERRED_LOCALE, - callback: (preferredLocale) => { - if (preferredLocale) { - translateLocal = (phrase, variables) => translate(preferredLocale, phrase, variables); - } - }, -}); - /** * Takes a full policy summary that is returned from the policySummaryList and simplifies it so we are only storing * the pieces of data that we need to in Onyx diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 5f039efd9ab4..46f79abc5c24 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -22,7 +22,7 @@ import {isReportMessageAttachment, sortReportsByLastVisited} from '../reportUtil import Timers from '../Timers'; import {dangerouslyGetReportActionsMaxSequenceNumber, isReportMissingActions} from './ReportActions'; import Growl from '../Growl'; -import {translate} from '../translate'; +import {translateLocal} from '../translate'; let currentUserEmail; let currentUserAccountID; @@ -59,16 +59,6 @@ Onyx.connect({ }, }); -let translateLocal = (phrase, variables) => translate(CONST.DEFAULT_LOCALE, phrase, variables); -Onyx.connect({ - key: ONYXKEYS.PREFERRED_LOCALE, - callback: (preferredLocale) => { - if (preferredLocale) { - translateLocal = (phrase, variables) => translate(preferredLocale, phrase, variables); - } - }, -}); - const typingWatchTimers = {}; /** diff --git a/src/libs/translate.js b/src/libs/translate.js index 2d548f8acd84..277f407fb1a8 100644 --- a/src/libs/translate.js +++ b/src/libs/translate.js @@ -1,9 +1,21 @@ import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; +import Onyx from 'react-native-onyx'; import Log from './Log'; import Config from '../CONFIG'; import translations from '../languages/translations'; import CONST from '../CONST'; +import ONYXKEYS from '../ONYXKEYS'; + +let preferredLocale = CONST.DEFAULT_LOCALE; +Onyx.connect({ + key: ONYXKEYS.PREFERRED_LOCALE, + callback: (val) => { + if (val) { + preferredLocale = val; + } + }, +}); /** * Return translated string for given locale and phrase @@ -53,9 +65,18 @@ function translate(locale = CONST.DEFAULT_LOCALE, phrase, variables = {}) { throw new Error(`${phrase} was not found in the default language`); } -export { +/** + * Uses the locale in this file updated by the Onyx subscriber. + * + * @param {String|Array} phrase + * @param {Object} [variables] + * @returns {String} + */ +function translateLocal(phrase, variables) { + return translate(preferredLocale, phrase, variables); +} - // Ignoring this lint error in case of we want to export more functions from this library - // eslint-disable-next-line import/prefer-default-export +export { translate, + translateLocal, }; diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 63835b0ddf98..e79638c5b874 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -85,37 +85,37 @@ class CompanyStep extends React.Component { } if (!this.isValidAddress(this.state.addressStreet)) { - Growl.error('Please enter a valid address street that is not a PO Box'); + Growl.error(this.props.translate('bankAccount.error.addressStreet')); return false; } if (!/[0-9]{5}(?:[- ][0-9]{4})?/.test(this.state.addressZipCode)) { - Growl.error('Please enter a valid zip code'); + Growl.error(this.props.translate('bankAccount.error.zipCode')); return false; } if (!Str.isValidURL(this.state.website)) { - Growl.error('Please enter a valid website'); + Growl.error(this.props.translate('bankAccount.error.website')); return false; } if (!/[0-9]{9}/.test(this.state.companyTaxID)) { - Growl.error('Please enter a valid Tax Id Number'); + Growl.error(this.props.translate('bankAccount.error.taxID')); return false; } if (!this.isValidDate(this.state.incorporationDate)) { - Growl.error('Please enter a valid incorporation date'); + Growl.error(this.props.translate('bankAccount.error.incorporationDate')); return false; } if (this.validateIndustryCode(this.state.industryCode)) { - Growl.error('Please enter a valid industry classification code'); + Growl.error(this.props.translate('bankAccount.error.industryCode')); return false; } if (!this.state.hasNoConnectionToCannabis) { - Growl.error('Please confirm company is not on the list of restricted businesses'); + Growl.error(this.props.translate('bankAccount.error.restrictedBusiness')); return false; } From 8adda3e29544f2222f7737b3092a565eac458602 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 15:06:08 -1000 Subject: [PATCH 09/17] move regex to CONST more translates --- src/CONST.js | 4 ++++ src/languages/en.js | 3 +++ src/libs/actions/BankAccounts.js | 8 ++++---- src/pages/ReimbursementAccount/CompanyStep.js | 8 ++++---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index c864d5a1ba71..f777d423a921 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -300,6 +300,10 @@ const CONST = { US_PHONE: /^\+1\d{10}$/, PHONE_E164_PLUS: /^\+?[1-9]\d{1,14}$/, NON_ALPHA_NUMERIC: /[^A-Za-z0-9+]/g, + PO_BOX: /\\b[P|p]?(OST|ost)?\\.?\\s*[O|o|0]?(ffice|FFICE)?\\.?\\s*[B|b][O|o|0]?[X|x]?\\.?\\s+[#]?(\\d+)\\b/, + ANY_VALUE: /^.+$/, + ZIP_CODE: /[0-9]{5}(?:[- ][0-9]{4})?/, + INDUSTRY_CODE: /^[0-9]{6}$/, }, GROWL: { diff --git a/src/languages/en.js b/src/languages/en.js index 71b404327ecc..be0c1c1b7c59 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -297,8 +297,11 @@ export default { zipCode: 'Please enter a valid zip code', addressStreet: 'Please enter a valid address street that is not a PO Box', incorporationDate: 'Please enter a valid incorporation date', + incorporationState: 'Please check Incorporation State and try again', industryCode: 'Please enter a valid industry classification code', restrictedBusiness: 'Please confirm company is not on the list of restricted businesses', + routingNumber: 'Please check Routing Number and try again', + companyType: 'Please check Company Type and try again', }, }, addPersonalBankAccountPage: { diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index bd06a9434c29..a7529b1059cf 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -631,12 +631,12 @@ function setupWithdrawalAccount(data) { if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_ROUTING_NUMBER || response.message === CONST.BANK_ACCOUNT.ERROR.MAX_ROUTING_NUMBER ) { - error = 'Please check Routing Number and try again'; - achData.subStep = 'manual'; + error = translateLocal('bankAccount.error.routingNumber'); + achData.subStep = CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL; } else if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_INCORPORATION_STATE) { - error = 'Please check Incorporation State and try again'; + error = translateLocal('bankAccount.error.incorporationState'); } else if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_INCORPORATION_TYPE) { - error = 'Please check Company Type and try again'; + error = translateLocal('bankAccount.error.companyType'); } else { console.error(response.message); } diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index e79638c5b874..81206dd2305c 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -50,13 +50,13 @@ class CompanyStep extends React.Component { * @returns {Boolean} */ isValidAddress(value) { - const isEmpty = !new RegExp('^.+$').test(value); + const isEmpty = !CONST.REGEX.ANY_VALUE.test(value); if (isEmpty) { return false; } // eslint-disable-next-line max-len - const isPoBox = new RegExp('\\b[P|p]?(OST|ost)?\\.?\\s*[O|o|0]?(ffice|FFICE)?\\.?\\s*[B|b][O|o|0]?[X|x]?\\.?\\s+[#]?(\\d+)\\b').test(value); + const isPoBox = CONST.REGEX.PO_BOX.test(value); return !isPoBox; } @@ -74,7 +74,7 @@ class CompanyStep extends React.Component { * @returns {Boolean} */ validateIndustryCode(code) { - return !/^[0-9]{6}$/.test(code); + return !CONST.REGEX.INDUSTRY_CODE.test(code); } validate() { @@ -89,7 +89,7 @@ class CompanyStep extends React.Component { return false; } - if (!/[0-9]{5}(?:[- ][0-9]{4})?/.test(this.state.addressZipCode)) { + if (!CONST.REGEX.ZIP_CODE.test(this.state.addressZipCode)) { Growl.error(this.props.translate('bankAccount.error.zipCode')); return false; } From d72e8f170b53efa38f6d5fe778af64f9dded47f7 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 18 Jun 2021 15:07:55 -1000 Subject: [PATCH 10/17] clean up --- src/pages/ReimbursementAccount/CompanyStep.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 81206dd2305c..5423086d7e69 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -46,22 +46,21 @@ class CompanyStep extends React.Component { /** * Validating that this is a valid address (PO boxes are not allowed) + * * @param {String} value * @returns {Boolean} */ isValidAddress(value) { - const isEmpty = !CONST.REGEX.ANY_VALUE.test(value); - if (isEmpty) { + if (!CONST.REGEX.ANY_VALUE.test(value)) { return false; } - // eslint-disable-next-line max-len - const isPoBox = CONST.REGEX.PO_BOX.test(value); - return !isPoBox; + return !CONST.REGEX.PO_BOX.test(value); } /** * Validate date fields + * * @param {String} date * @returns {Boolean} true if valid */ From 55fb9bf00e5dbabc4cc7f3837b6f1a17ff43b371 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Mon, 21 Jun 2021 07:42:20 -1000 Subject: [PATCH 11/17] add ValidationUtils --- src/libs/ValidationUtils.js | 40 +++++++++++++++++++ src/pages/ReimbursementAccount/CompanyStep.js | 39 ++---------------- 2 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 src/libs/ValidationUtils.js diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js new file mode 100644 index 000000000000..2a4f0061da23 --- /dev/null +++ b/src/libs/ValidationUtils.js @@ -0,0 +1,40 @@ +import moment from 'moment'; +import CONST from '../CONST'; + +/** + * Validating that this is a valid address (PO boxes are not allowed) + * + * @param {String} value + * @returns {Boolean} + */ +function isValidAddress(value) { + if (!CONST.REGEX.ANY_VALUE.test(value)) { + return false; + } + + return !CONST.REGEX.PO_BOX.test(value); +} + +/** + * Validate date fields + * + * @param {String} date + * @returns {Boolean} true if valid + */ +function isValidDate(date) { + return moment(date).isValid(); +} + +/** + * @param {String} code + * @returns {Boolean} + */ +function isValidIndustryCode(code) { + return CONST.REGEX.INDUSTRY_CODE.test(code); +} + +export { + isValidAddress, + isValidDate, + isValidIndustryCode, +}; diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 5423086d7e69..4158cefc6a2e 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -19,6 +19,7 @@ import Picker from '../../components/Picker'; import StatePicker from '../../components/StatePicker'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import Growl from '../../libs/Growl'; +import {isValidAddress, isValidDate, isValidIndustryCode} from '../../libs/ValidationUtils'; class CompanyStep extends React.Component { constructor(props) { @@ -44,38 +45,6 @@ class CompanyStep extends React.Component { }; } - /** - * Validating that this is a valid address (PO boxes are not allowed) - * - * @param {String} value - * @returns {Boolean} - */ - isValidAddress(value) { - if (!CONST.REGEX.ANY_VALUE.test(value)) { - return false; - } - - return !CONST.REGEX.PO_BOX.test(value); - } - - /** - * Validate date fields - * - * @param {String} date - * @returns {Boolean} true if valid - */ - isValidDate(date) { - return moment(date).isValid(); - } - - /** - * @param {String} code - * @returns {Boolean} - */ - validateIndustryCode(code) { - return !CONST.REGEX.INDUSTRY_CODE.test(code); - } - validate() { // @TODO check more than just the password if (!this.state.password.trim()) { @@ -83,7 +52,7 @@ class CompanyStep extends React.Component { return false; } - if (!this.isValidAddress(this.state.addressStreet)) { + if (!isValidAddress(this.state.addressStreet)) { Growl.error(this.props.translate('bankAccount.error.addressStreet')); return false; } @@ -103,12 +72,12 @@ class CompanyStep extends React.Component { return false; } - if (!this.isValidDate(this.state.incorporationDate)) { + if (!isValidDate(this.state.incorporationDate)) { Growl.error(this.props.translate('bankAccount.error.incorporationDate')); return false; } - if (this.validateIndustryCode(this.state.industryCode)) { + if (!isValidIndustryCode(this.state.industryCode)) { Growl.error(this.props.translate('bankAccount.error.industryCode')); return false; } From 7c64a216505e7ef18f33149680bfd61bce23f155 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Mon, 21 Jun 2021 07:42:44 -1000 Subject: [PATCH 12/17] remove todo --- src/pages/ReimbursementAccount/CompanyStep.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 4158cefc6a2e..16900da54d09 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -46,7 +46,6 @@ class CompanyStep extends React.Component { } validate() { - // @TODO check more than just the password if (!this.state.password.trim()) { Growl.error(this.props.translate('common.passwordCannotBeBlank')); return false; From 90db16ae7d14348d930a317a5db5dc10065b45da Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Mon, 21 Jun 2021 08:13:21 -1000 Subject: [PATCH 13/17] add disabled button state --- src/CONST.js | 1 + src/components/MenuItem.js | 52 ++++++++++++------- src/languages/en.js | 1 + src/languages/es.js | 14 +++++ src/libs/getButtonState.js | 7 ++- .../ReimbursementAccount/BankAccountStep.js | 22 ++++---- src/styles/styles.js | 6 +++ 7 files changed, 74 insertions(+), 29 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index f777d423a921..a4573f8af28e 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -85,6 +85,7 @@ const CONST = { HOVERED: 'hovered', PRESSED: 'pressed', COMPLETE: 'complete', + DISABLED: 'disabled', }, COUNTRY: { US: 'US', diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 30b3621a0f4c..facfa92a1c67 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -45,6 +45,9 @@ const propTypes = { /** The fill color to pass into the icon. */ iconFill: PropTypes.string, + + /** Should we disable this menu item? */ + disabled: PropTypes.bool, }; const defaultProps = { @@ -58,6 +61,7 @@ const defaultProps = { iconRight: ArrowRight, iconStyles: [], iconFill: undefined, + disabled: false, }; const MenuItem = ({ @@ -73,12 +77,19 @@ const MenuItem = ({ description, iconStyles, iconFill, + disabled, }) => ( { + if (disabled) { + return; + } + + onPress(e); + }} style={({hovered, pressed}) => ([ styles.createMenuItem, - getButtonBackgroundColorStyle(getButtonState(hovered, pressed)), + getButtonBackgroundColorStyle(getButtonState(hovered, pressed, success, disabled)), wrapperStyle, ])} > @@ -86,22 +97,22 @@ const MenuItem = ({ <> {icon && ( - - - + + + )} - + {title} {description && ( @@ -112,9 +123,12 @@ const MenuItem = ({ {shouldShowRightIcon && ( - - - + + + )} )} diff --git a/src/languages/en.js b/src/languages/en.js index be0c1c1b7c59..45358a6406f2 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -302,6 +302,7 @@ export default { restrictedBusiness: 'Please confirm company is not on the list of restricted businesses', routingNumber: 'Please check Routing Number and try again', companyType: 'Please check Company Type and try again', + tooManyAttempts: 'Due to a high number of login attempts, this option has been temporarily disabled for 24 hours. Please try again later or manually enter details instead.', }, }, addPersonalBankAccountPage: { diff --git a/src/languages/es.js b/src/languages/es.js index 8a0fd6565c1e..9bca5f4f3c05 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -283,6 +283,20 @@ export default { checkHelpLine: 'Su número de ruta y número de cuenta se pueden encontrar en un cheque para la cuenta.', 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!`, + error: { + noBankAccountAvailable: 'Sorry, no bank account is available', + taxID: 'Please enter a valid Tax ID Number', + website: 'Please enter a valid website', + zipCode: 'Please enter a valid zip code', + addressStreet: 'Please enter a valid address street that is not a PO Box', + incorporationDate: 'Please enter a valid incorporation date', + incorporationState: 'Please check Incorporation State and try again', + industryCode: 'Please enter a valid industry classification code', + restrictedBusiness: 'Please confirm company is not on the list of restricted businesses', + routingNumber: 'Please check Routing Number and try again', + companyType: 'Please check Company Type and try again', + tooManyAttempts: 'Due to a high number of login attempts, this option has been temporarily disabled for 24 hours. Please try again later or manually enter details instead.', + }, }, addPersonalBankAccountPage: { enterPassword: 'Escribe una contraseña', diff --git a/src/libs/getButtonState.js b/src/libs/getButtonState.js index 51c006211ab4..682aade80b5a 100644 --- a/src/libs/getButtonState.js +++ b/src/libs/getButtonState.js @@ -6,9 +6,14 @@ import CONST from '../CONST'; * @param {Boolean} [isHovered] * @param {Boolean} [isPressed] * @param {Boolean} [isComplete] + * @param {Boolean} [isDisabled] * @returns {String} */ -export default function (isHovered = false, isPressed = false, isComplete = false) { +export default function (isHovered = false, isPressed = false, isComplete = false, isDisabled = false) { + if (isDisabled) { + return CONST.BUTTON_STATES.DISABLED; + } + if (isComplete) { return CONST.BUTTON_STATES.COMPLETE; } diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 7e6dfc8443f9..f519ffe2c73a 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -123,15 +123,19 @@ class BankAccountStep extends React.Component { {this.props.translate('bankAccount.toGetStarted')} - {!this.props.isPlaidDisabled && ( - { - this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}); - }} - shouldShowRightIcon - /> + { + this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}); + }} + disabled={this.props.isPlaidDisabled} + shouldShowRightIcon + /> + {this.props.isPlaidDisabled && ( + + {this.props.translate('bankAccount.error.tooManyAttempts')} + )} Date: Mon, 21 Jun 2021 08:18:38 -1000 Subject: [PATCH 14/17] add onSubmitEditing callback to password input --- src/pages/ReimbursementAccount/CompanyStep.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 16900da54d09..5a2a6a27a6a4 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -213,6 +213,7 @@ class CompanyStep extends React.Component { textContentType="password" onChangeText={password => this.setState({password})} value={this.state.password} + onSubmitEditing={this.submit} /> Date: Mon, 21 Jun 2021 11:09:36 -1000 Subject: [PATCH 15/17] fix translations --- src/languages/en.js | 6 +++--- src/languages/es.js | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 45358a6406f2..6f373f4eb2f6 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -297,11 +297,11 @@ export default { zipCode: 'Please enter a valid zip code', addressStreet: 'Please enter a valid address street that is not a PO Box', incorporationDate: 'Please enter a valid incorporation date', - incorporationState: 'Please check Incorporation State and try again', + incorporationState: 'Please enter a valid Incorporation State', industryCode: 'Please enter a valid industry classification code', restrictedBusiness: 'Please confirm company is not on the list of restricted businesses', - routingNumber: 'Please check Routing Number and try again', - companyType: 'Please check Company Type and try again', + routingNumber: 'Please enter a valid Routing Number', + companyType: 'Please enter a valid Company Type', tooManyAttempts: 'Due to a high number of login attempts, this option has been temporarily disabled for 24 hours. Please try again later or manually enter details instead.', }, }, diff --git a/src/languages/es.js b/src/languages/es.js index 9bca5f4f3c05..63a916bafdfc 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -284,18 +284,18 @@ export default { 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!`, error: { - noBankAccountAvailable: 'Sorry, no bank account is available', - taxID: 'Please enter a valid Tax ID Number', - website: 'Please enter a valid website', - zipCode: 'Please enter a valid zip code', - addressStreet: 'Please enter a valid address street that is not a PO Box', - incorporationDate: 'Please enter a valid incorporation date', - incorporationState: 'Please check Incorporation State and try again', - industryCode: 'Please enter a valid industry classification code', - restrictedBusiness: 'Please confirm company is not on the list of restricted businesses', - routingNumber: 'Please check Routing Number and try again', - companyType: 'Please check Company Type and try again', - tooManyAttempts: 'Due to a high number of login attempts, this option has been temporarily disabled for 24 hours. Please try again later or manually enter details instead.', + noBankAccountAvailable: 'Lo sentimos, no hay ninguna cuenta bancaria disponible', + taxID: 'Ingrese un número de identificación fiscal válido', + website: 'Ingrese un sitio web válido', + zipCode: 'Ingrese un código postal válido', + addressStreet: 'Ingrese una calle de dirección válida que no sea un apartado postal', + incorporationDate: 'Ingrese una fecha de incorporación válida', + incorporationState: 'Ingrese un estado de incorporación válido', + industryCode: 'Ingrese un código de clasificación de industria válido', + restrictedBusiness: 'Confirme que la empresa no está en la lista de negocios restringidos', + routingNumber: 'Ingrese un número de ruta válido', + companyType: 'Ingrese un tipo de compañía válido', + tooManyAttempts: 'Debido a la gran cantidad de intentos de inicio de sesión, esta opción se ha desactivado temporalmente durante 24 horas. Vuelva a intentarlo más tarde o introduzca los detalles manualmente.', }, }, addPersonalBankAccountPage: { From 8631089bda81a5a4cc2d51325c134037b3cc6947 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Mon, 21 Jun 2021 11:22:43 -1000 Subject: [PATCH 16/17] add placeholder text --- src/languages/en.js | 2 ++ src/languages/es.js | 2 ++ src/pages/ReimbursementAccount/CompanyStep.js | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/languages/en.js b/src/languages/en.js index 6f373f4eb2f6..576e9ead16fb 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -365,6 +365,8 @@ export default { industryClassificationCode: 'Industry Classification Code', confirmCompanyIsNot: 'I confirm that this company is not on the', listOfRestrictedBusinesses: 'list of restricted businesses', + incorporationDatePlaceholder: 'Start date (yyyy-mm-dd)', + companyPhonePlaceholder: '10 digits, no hyphens', }, requestorStep: { headerTitle: 'Requestor Information', diff --git a/src/languages/es.js b/src/languages/es.js index 63a916bafdfc..4b2f9e8d2548 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -365,5 +365,7 @@ export default { industryClassificationCode: 'Código de Clasificación Industrial', confirmCompanyIsNot: 'Confirmo que esta empresa no está en el', listOfRestrictedBusinesses: 'lista de negocios restringidos', + incorporationDatePlaceholder: 'Fecha de inicio (aaaa-mm-dd)', + companyPhonePlaceholder: '10 dígitos, sin guiones', }, }; diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 5a2a6a27a6a4..de27aa716abf 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -155,6 +155,7 @@ class CompanyStep extends React.Component { keyboardType={CONST.KEYBOARD_TYPE.PHONE_PAD} onChangeText={companyPhone => this.setState({companyPhone})} value={this.state.companyPhone} + placeholder={this.props.translate('companyStep.companyPhonePlaceholder')} /> this.setState({incorporationDate})} value={this.state.incorporationDate} + placeholder={this.props.translate('companyStep.incorporationDatePlaceholder')} /> From ba7e6586eafb28c12e1a8ec18034ae06579a9192 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Mon, 21 Jun 2021 15:08:11 -1000 Subject: [PATCH 17/17] Fix up error message --- .../ReimbursementAccountPage.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index 69ae6afe2352..d03b2f385177 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -3,7 +3,7 @@ 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 {View} from 'react-native'; import PropTypes from 'prop-types'; import ScreenWrapper from '../../components/ScreenWrapper'; import {fetchFreePlanVerifiedBankAccount} from '../../libs/actions/BankAccounts'; @@ -16,6 +16,7 @@ import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize import compose from '../../libs/compose'; import styles from '../../styles/styles'; import KeyboardAvoidingView from '../../components/KeyboardAvoidingView'; +import Text from '../../components/Text'; // Steps import BankAccountStep from './BankAccountStep'; @@ -23,6 +24,7 @@ import CompanyStep from './CompanyStep'; import RequestorStep from './RequestorStep'; import ValidationStep from './ValidationStep'; import BeneficialOwnersStep from './BeneficialOwnersStep'; +import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; const propTypes = { /** List of betas */ @@ -109,10 +111,11 @@ class ReimbursementAccountPage extends React.Component { return ; } + let error; const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, CONST.SMS.DOMAIN); if (userHasPhonePrimaryEmail) { - return ( + error = ( {this.props.translate('bankAccount.hasPhoneLoginError')} @@ -123,7 +126,7 @@ class ReimbursementAccountPage extends React.Component { if (throttledDate) { const throttledEnd = moment().add(24, 'hours'); if (moment() < throttledEnd) { - return ( + error = ( {this.props.translate('bankAccount.hasBeenThrottledError', { @@ -135,6 +138,18 @@ class ReimbursementAccountPage extends React.Component { } } + if (error) { + return ( + + + {error} + + ); + } + // 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. We can also // specify a specific step to navigate to by using route params.