diff --git a/src/CONST.js b/src/CONST.js index 8e047d087b5a..a4573f8af28e 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -6,10 +6,25 @@ 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', }, BANK_ACCOUNT: { + PLAID: { + ALLOWED_THROTTLED_COUNT: 2, + ERROR: { + 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', @@ -70,6 +85,7 @@ const CONST = { HOVERED: 'hovered', PRESSED: 'pressed', COMPLETE: 'complete', + DISABLED: 'disabled', }, COUNTRY: { US: 'US', @@ -170,6 +186,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}, @@ -284,6 +301,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/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/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/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..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 */ @@ -230,7 +229,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/languages/en.js b/src/languages/en.js index 5d6d6ddbb897..576e9ead16fb 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -290,6 +290,20 @@ 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', + 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 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.', + }, }, addPersonalBankAccountPage: { enterPassword: 'Enter password', @@ -351,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 8a0fd6565c1e..4b2f9e8d2548 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: '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: { enterPassword: 'Escribe una contraseña', @@ -351,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/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/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/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 3a5fa92ef757..a7529b1059cf 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -9,6 +9,25 @@ 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. + * + * @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 +44,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 +84,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.error(translateLocal('bankAccount.error.noBankAccountAvailable')); + } + Onyx.merge(ONYXKEYS.PLAID_BANK_ACCOUNTS, { error: { title: response.title, @@ -274,41 +322,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 +343,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 +370,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 +439,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(() => { @@ -604,6 +626,22 @@ function setupWithdrawalAccount(data) { if (response.jsonCode === 666 || response.jsonCode === 404) { error = response.message; } + + if (response.jsonCode === 402) { + if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_ROUTING_NUMBER + || response.message === CONST.BANK_ACCOUNT.ERROR.MAX_ROUTING_NUMBER + ) { + 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 = translateLocal('bankAccount.error.incorporationState'); + } else if (response.message === CONST.BANK_ACCOUNT.ERROR.MISSING_INCORPORATION_TYPE) { + error = translateLocal('bankAccount.error.companyType'); + } else { + console.error(response.message); + } + } + if (lodashGet(achData, CONST.BANK_ACCOUNT.VERIFICATIONS.THROTTLED)) { achData.disableFields = true; } @@ -613,7 +651,7 @@ function setupWithdrawalAccount(data) { goToWithdrawalAccountSetupStep(nextStep, achData); if (error) { - Growl.show(`Error setting up account: ${error}`, CONST.GROWL.ERROR, 5000); + Growl.error(error, 5000); } }); } diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 15e1f69ccfa0..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 @@ -141,7 +131,7 @@ function invite(login, welcomeNote, policyID) { errorMessage += ` ${translateLocal('workspace.invite.pleaseEnterValidLogin')}`; } - Growl.show(errorMessage, CONST.GROWL.ERROR, 5000); + Growl.error(errorMessage, 5000); }); } @@ -156,7 +146,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..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 = {}; /** @@ -1057,7 +1047,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/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/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/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 15674eeee1aa..f519ffe2c73a 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -129,8 +129,14 @@ class BankAccountStep extends React.Component { onPress={() => { this.setState({bankAccountAddMethod: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}); }} + disabled={this.props.isPlaidDisabled} shouldShowRightIcon /> + {this.props.isPlaidDisabled && ( + + {this.props.translate('bankAccount.error.tooManyAttempts')} + + )} 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')} /> @@ -175,6 +215,7 @@ class CompanyStep extends React.Component { textContentType="password" onChangeText={password => this.setState({password})} value={this.state.password} + onSubmitEditing={this.submit} /> ; } + 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. @@ -144,7 +159,10 @@ class ReimbursementAccountPage extends React.Component { {currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT && ( - + )} {currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && ( 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; } diff --git a/src/styles/styles.js b/src/styles/styles.js index 27ab26585723..a97f35d25238 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -361,6 +361,10 @@ const styles = { textAlignVertical: 'center', }, + disabledText: { + color: colors.gray3, + }, + disabledTextInput: { backgroundColor: colors.gray1, color: colors.gray3, @@ -1800,6 +1804,7 @@ function getButtonBackgroundColorStyle(buttonState = CONST.BUTTON_STATES.DEFAULT return {backgroundColor: themeColors.buttonHoveredBG}; case CONST.BUTTON_STATES.PRESSED: return {backgroundColor: themeColors.buttonPressedBG}; + case CONST.BUTTON_STATES.DISABLED: case CONST.BUTTON_STATES.DEFAULT: default: return {}; @@ -1821,6 +1826,7 @@ function getIconFillColor(buttonState = CONST.BUTTON_STATES.DEFAULT) { case CONST.BUTTON_STATES.COMPLETE: return themeColors.iconSuccessFill; case CONST.BUTTON_STATES.DEFAULT: + case CONST.BUTTON_STATES.DISABLED: default: return themeColors.icon; }