Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Validation Step #3587

Merged
merged 35 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cbbb536
Add header and make sure things are loading
ctkochan22 Jun 15, 2021
68d9829
get props and state
ctkochan22 Jun 15, 2021
3bbb98b
get methods talking
ctkochan22 Jun 15, 2021
ebcc2e4
Add amound 2 and 3 and concatoate them
ctkochan22 Jun 15, 2021
af09ae8
Add a call to WebAPI
ctkochan22 Jun 15, 2021
ea09cfa
remove debugging logs
ctkochan22 Jun 15, 2021
94ef391
export validate method
ctkochan22 Jun 15, 2021
0bb78c3
Validate amounts
ctkochan22 Jun 16, 2021
37433a4
Add clarification
ctkochan22 Jun 16, 2021
adb440f
Merge branch 'main' into ckt_validationStep
ctkochan22 Jun 16, 2021
c0fd259
grab failed reimbursement
ctkochan22 Jun 17, 2021
4af47ae
Pull bankaccountID first
ctkochan22 Jun 17, 2021
794a1ae
Nest all validation in PENDING
ctkochan22 Jun 17, 2021
fb6458a
Add images
ctkochan22 Jun 17, 2021
1fab732
fix image url
ctkochan22 Jun 17, 2021
a4b0c47
merge master
ctkochan22 Jun 17, 2021
1ac0380
Start getting all the copy lined up
ctkochan22 Jun 17, 2021
b1ff46b
Fix error
ctkochan22 Jun 18, 2021
6b4a465
dismiss modal upon success
ctkochan22 Jun 18, 2021
5591738
merge main
ctkochan22 Jun 19, 2021
ea1d45b
Fix merge errors
ctkochan22 Jun 19, 2021
cebc4a3
remove debuggint logs
ctkochan22 Jun 19, 2021
6054889
Pass maxAttemptsReached
ctkochan22 Jun 19, 2021
ef83c5d
Fix more feedback and style
ctkochan22 Jun 21, 2021
71ad002
Merge branch 'ckt_validationStep' of github.com:Expensify/Expensify.c…
ctkochan22 Jun 21, 2021
723f02c
adjust spacing and add copy to constant
ctkochan22 Jun 21, 2021
7335f35
Add spanish
ctkochan22 Jun 21, 2021
7cd6489
Fix extra padding
ctkochan22 Jun 21, 2021
43cd571
Adjust sizing of verifying gif
ctkochan22 Jun 21, 2021
8c2cfb4
fix lint and alphabetize export
ctkochan22 Jun 21, 2021
4bf2b81
Update copy and add red text
ctkochan22 Jun 22, 2021
da55c9b
travis style
ctkochan22 Jun 22, 2021
adaf7fb
fix final review
ctkochan22 Jun 22, 2021
1bed83b
Merge and fix conflicts
ctkochan22 Jun 22, 2021
e0cecca
fix conflicts
marcaaron Jun 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
ctkochan22 marked this conversation as resolved.
Show resolved Hide resolved

/** 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