Skip to content

Commit

Permalink
Merge pull request #3587 from Expensify/ckt_validationStep
Browse files Browse the repository at this point in the history
Create Validation Step
  • Loading branch information
NikkiWines authored Jun 22, 2021
2 parents 2792082 + e0cecca commit 318ffa0
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 123 deletions.
2 changes: 2 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const CONST = {
IBAN: /^[A-Za-z0-9]{2,30}$/,
SWIFT_BIC: /^[A-Za-z0-9]{8,11}$/,
},
VERIFICATION_MAX_ATTEMPTS: 7,
},
INCORPORATION_TYPES: {
LLC: 'LLC',
Expand Down Expand Up @@ -191,6 +192,7 @@ const CONST = {
TIMEZONE: 'timeZone',
FREE_PLAN_BANK_ACCOUNT_ID: 'expensify_freePlanBankAccountID',
ACH_DATA_THROTTLED: 'expensify_ACHData_throttled',
FAILED_BANK_ACCOUNT_VALIDATIONS_PREFIX: 'private_failedBankValidations_',
BANK_ACCOUNT_GET_THROTTLED: 'private_throttledHistory_BankAccount_Get',
},
DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'},
Expand Down
8 changes: 8 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,14 @@ export default {
ssnLast4: 'Last 4 Digits of SSN',
isAuthorized: 'I am authorized to use my company bank account for business spend',
},
validationStep: {
headerTitle: 'Validate',
buttonText: 'Finish Setup',
maxAttemptError: 'Validation for this bank account has been disabled due to too many incorrect attempts. Please contact us.',
description: 'A day or two after you add your account to Expensify we send three (3) transactions to your account. They have a merchant line like "Expensify, Inc. Validation"',
descriptionCTA: 'Please enter each transaction amount in the fields below. Example: 1.51',
verifyingDescription: 'We\'re taking a look at your information and will have you onto next steps in just a few seconds.',
},
beneficialOwnersStep: {
beneficialOwners: 'Beneficial Owners',
additionalInformation: 'Additional Information',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,14 @@ export default {
incorporationDatePlaceholder: 'Fecha de inicio (aaaa-mm-dd)',
companyPhonePlaceholder: '10 dígitos, sin guiones',
},
validationStep: {
headerTitle: 'Validar',
buttonText: 'Finalizar Configuración',
maxAttemptError: 'Se ha inhabilitado la validación de esta cuenta bancaria, debido a demasiados intentos incorrectos. Por favor contáctenos.',
description: 'Uno o dos días después de agregar su cuenta a Expensify, enviamos tres (3) transacciones a su cuenta. Tienen una línea comercial como "Expensify, Inc. Validation"',
descriptionCTA: 'Ingrese el monto de cada transacción en los campos a continuación. Ejemplo: 1.51',
verifyingDescription: 'Estamos revisando su información y lo llevaremos a los siguientes pasos en solo unos segundos.',
},
requestCallPage: {
requestACall: 'Llámame por teléfono',
description: '¿Necesitas ayuda configurando tu cuenta? Nuestro equipo de guías puede ayudarte.',
Expand Down
7 changes: 7 additions & 0 deletions src/libs/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,12 @@ function BankAccount_Create(parameters) {
return Network.post(commandName, parameters, CONST.NETWORK.METHOD.POST, true);
}

function BankAccount_Validate(parameters) {
const commandName = 'ValidateBankAccount';
requireParameters(['bankAccountID', 'validateCode'], parameters, commandName);
return Network.post(commandName, parameters, CONST.NETWORK.METHOD.POST);
}

/**
* @param {*} parameters
* @returns {Promise}
Expand Down Expand Up @@ -960,6 +966,7 @@ export {
BankAccount_Create,
BankAccount_Get,
BankAccount_SetupWithdrawal,
BankAccount_Validate,
ChangePassword,
CreateChatReport,
CreateLogin,
Expand Down
275 changes: 161 additions & 114 deletions src/libs/actions/BankAccounts.js

Large diffs are not rendered by default.

22 changes: 16 additions & 6 deletions src/pages/ReimbursementAccount/ReimbursementAccountPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ const propTypes = {
/** Step of the setup flow that we are on. Determines which view is presented. */
currentStep: PropTypes.string,
}),

/** Disable validation button if max attempts exceeded */
maxAttemptsReached: PropTypes.bool,
}),

/** Current session for the user */
Expand Down Expand Up @@ -111,11 +114,11 @@ class ReimbursementAccountPage extends React.Component {
return <FullScreenLoadingIndicator visible />;
}

let error;
let errorComponent;
const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, CONST.SMS.DOMAIN);

if (userHasPhonePrimaryEmail) {
error = (
errorComponent = (
<View style={[styles.m5]}>
<Text>{this.props.translate('bankAccount.hasPhoneLoginError')}</Text>
</View>
Expand All @@ -126,7 +129,7 @@ class ReimbursementAccountPage extends React.Component {
if (throttledDate) {
const throttledEnd = moment().add(24, 'hours');
if (moment() < throttledEnd) {
error = (
errorComponent = (
<View style={[styles.m5]}>
<Text>
{this.props.translate('bankAccount.hasBeenThrottledError', {
Expand All @@ -138,18 +141,21 @@ class ReimbursementAccountPage extends React.Component {
}
}

if (error) {
if (errorComponent) {
return (
<ScreenWrapper>
<HeaderWithCloseButton
title={this.props.translate('bankAccount.addBankAccount')}
onCloseButtonPress={Navigation.dismissModal}
/>
{error}
{errorComponent}
</ScreenWrapper>
);
}

const error = lodashGet(this.props, 'reimbursementAccount.error');
const maxAttemptsReached = lodashGet(this.props, 'reimbursementAccount.maxAttemptsReached');

// 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.
Expand All @@ -174,7 +180,11 @@ class ReimbursementAccountPage extends React.Component {
<BeneficialOwnersStep companyName={achData.companyName} />
)}
{currentStep === CONST.BANK_ACCOUNT.STEP.VALIDATION && (
<ValidationStep />
<ValidationStep
achData={this.props.reimbursementAccount.achData}
maxAttemptsReached={maxAttemptsReached}
error={error}
/>
)}
</KeyboardAvoidingView>
</ScreenWrapper>
Expand Down
182 changes: 179 additions & 3 deletions src/pages/ReimbursementAccount/ValidationStep.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,181 @@
import React from 'react';
import {View} from 'react-native';
import {Image, View} from 'react-native';
import PropTypes from 'prop-types';
import Str from 'expensify-common/lib/str';
import _ from 'underscore';
import styles from '../../styles/styles';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';

const ValidationStep = () => <View />;
export default ValidationStep;
import {validateBankAccount} from '../../libs/actions/BankAccounts';

import Button from '../../components/Button';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import Navigation from '../../libs/Navigation/Navigation';
import TextInputWithLabel from '../../components/TextInputWithLabel';
import Text from '../../components/Text';
import BankAccount from '../../libs/models/BankAccount';
import CONST from '../../CONST';

const propTypes = {
...withLocalizePropTypes,

/** Additional data for the account in setup */
achData: PropTypes.shape({

/** Bank account ID of the VBA that we are validating is required */
bankAccountID: PropTypes.number.isRequired,

/** State of bank account */
state: PropTypes.string,
}).isRequired,

/** Error message to display to user */
error: PropTypes.string,

/** Disable validation button if max attempts exceeded */
maxAttemptsReached: PropTypes.bool,
};

const defaultProps = {
error: '',
maxAttemptsReached: false,
};

class ValidationStep extends React.Component {
constructor(props) {
super(props);

this.submit = this.submit.bind(this);

this.verifyingUrl = `${CONST.CLOUDFRONT_URL}/images/icons/emptystates/emptystate_reviewing.gif`;

this.state = {
amount1: '',
amount2: '',
amount3: '',
error: '',
};
}

submit() {
const amount1 = this.filterInput(this.state.amount1);
const amount2 = this.filterInput(this.state.amount2);
const amount3 = this.filterInput(this.state.amount3);

// If amounts are all non-zeros, submit amounts to API
if (amount1 && amount2 && amount3) {
const validateCode = [amount1, amount2, amount3].join(',');

// Send valid amounts to BankAccountAPI::validateBankAccount in Web-Expensify
validateBankAccount(this.props.achData.bankAccountID, validateCode);
return;
}

// If any values are falsey, indicate to user that inputs are invalid
this.setState({error: 'Invalid amounts'});
}

/**
* Filter input for validation amount
* Anything that isn't a number is returned as an empty string
* Any dollar amount (e.g. 1.12) will be returned as 112
*
* @param {String} amount field input
*
* @returns {String}
*/
filterInput(amount) {
let value = amount.trim();
if (value === '' || !Math.abs(Str.fromUSDToNumber(value)) || _.isNaN(Number(value))) {
return '';
}

// If the user enters the values in dollars, convert it to the respective cents amount
if (_.contains(value, '.')) {
value = Str.fromUSDToNumber(value);
}

return value;
}

render() {
let errorMessage = this.state.error ? this.state.error : this.props.error;
if (this.props.maxAttemptsReached) {
errorMessage = this.props.translate('validationStep.maxAttemptError');
}

const state = this.props.achData.state;
return (
<View style={[styles.flex1, styles.justifyContentBetween]}>
<HeaderWithCloseButton
title={this.props.translate('validationStep.headerTitle')}
onCloseButtonPress={Navigation.dismissModal}
/>
{state === BankAccount.STATE.PENDING && (
<View style={[styles.flex1, styles.mt2]}>
<View style={[styles.mb2]}>
<Text style={[styles.mh5, styles.mb5]}>
{this.props.translate('validationStep.description')}
</Text>
<Text style={[styles.mh5, styles.mb2]}>
{this.props.translate('validationStep.descriptionCTA')}
</Text>
</View>
<View style={[styles.m5, styles.flex1]}>
<TextInputWithLabel
containerStyles={[styles.mb1]}
placeholder="1.52"
keyboardType="number-pad"
value={this.state.amount1}
onChangeText={amount1 => this.setState({amount1})}
/>
<TextInputWithLabel
containerStyles={[styles.mb1]}
placeholder="1.53"
keyboardType="number-pad"
value={this.state.amount2}
onChangeText={amount2 => this.setState({amount2})}
/>
<TextInputWithLabel
containerStyles={[styles.mb1]}
placeholder="1.54"
keyboardType="number-pad"
value={this.state.amount3}
onChangeText={amount3 => this.setState({amount3})}
/>
{errorMessage && (
<Text style={[styles.mb5, styles.textDanger]}>
{errorMessage}
</Text>
)}
</View>
<Button
success
text={this.props.translate('validationStep.buttonText')}
style={[styles.m5]}
onPress={this.submit}
isDisabled={this.props.maxAttemptsReached}
/>
</View>
)}
{state === BankAccount.STATE.VERIFYING && (
<View style={[styles.flex1]}>
<Image
source={{uri: this.verifyingUrl}}
style={[styles.workspaceInviteWelcome]}
resizeMode="center"
/>
<Text style={[styles.mh5, styles.mb5]}>
{this.props.translate('validationStep.verifyingDescription')}
</Text>
</View>
)}
</View>
);
}
}

ValidationStep.propTypes = propTypes;
ValidationStep.defaultProps = defaultProps;

export default withLocalize(ValidationStep);
4 changes: 4 additions & 0 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ const styles = {
color: themeColors.heading,
},

textDanger: {
color: colors.red,
},

button: {
backgroundColor: themeColors.buttonDefaultBG,
borderRadius: variables.componentBorderRadiusNormal,
Expand Down

0 comments on commit 318ffa0

Please sign in to comment.