diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index c97e2e24b1ec..4e1b2f69c828 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -115,6 +115,9 @@ export default {
// Stores information about additional details form entry
WALLET_ADDITIONAL_DETAILS: 'walletAdditionalDetails',
+ // Stores values put into the additional details step of the wallet KYC flow
+ WALLET_ADDITIONAL_DETAILS_DRAFT: 'walletAdditionalDetailsDraft',
+
// Object containing Wallet terms step state
WALLET_TERMS: 'walletTerms',
diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js
index 855f7f7fe4d2..f8ef4e76c186 100644
--- a/src/components/AddressSearch.js
+++ b/src/components/AddressSearch.js
@@ -30,9 +30,10 @@ const propTypes = {
...withLocalizePropTypes,
};
+
const defaultProps = {
value: '',
- containerStyles: null,
+ containerStyles: [],
};
// Do not convert to class component! It's been tried before and presents more challenges than it's worth.
diff --git a/src/components/FormScrollView.js b/src/components/FormScrollView.js
new file mode 100644
index 000000000000..aa84bfefcc2f
--- /dev/null
+++ b/src/components/FormScrollView.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {ScrollView} from 'react-native';
+import styles from '../styles/styles';
+
+const propTypes = {
+ /** Form elements */
+ children: PropTypes.node.isRequired,
+};
+
+const FormScrollView = React.forwardRef((props, ref) => (
+
+ {props.children}
+
+));
+
+FormScrollView.propTypes = propTypes;
+export default FormScrollView;
diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js
index c782868a3435..59770385d4cd 100644
--- a/src/libs/ValidationUtils.js
+++ b/src/libs/ValidationUtils.js
@@ -316,4 +316,5 @@ export {
isValidLengthForFirstOrLastName,
isValidPaypalUsername,
isValidRoutingNumber,
+ isValidSSNLastFour,
};
diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js
index 3f2a4aa74a3a..fd441d8ed64e 100644
--- a/src/libs/actions/Wallet.js
+++ b/src/libs/actions/Wallet.js
@@ -28,15 +28,26 @@ function fetchOnfidoToken() {
}
/**
- * Privately used to update the additionalDetails object in Onyx (which will have various effects on the UI)
- *
* @param {Boolean} loading whether we are making the API call to validate the user's provided personal details
- * @param {String[]} [errorFields] an array of field names that should display errors in the UI
- * @param {String} [additionalErrorMessage] an additional error message to display in the UI
* @private
*/
-function setAdditionalDetailsStep(loading, errorFields = null, additionalErrorMessage = '') {
- Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {loading, errorFields, additionalErrorMessage});
+function setAdditionalDetailsLoading(loading) {
+ Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {loading});
+}
+
+/**
+ * @param {Object} errorFields
+ */
+function setAdditionalDetailsErrors(errorFields) {
+ Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {errorFields: null});
+ Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {errorFields});
+}
+
+/**
+ * @param {String} additionalErrorMessage
+ */
+function setAdditionalDetailsErrorMessage(additionalErrorMessage) {
+ Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {additionalErrorMessage});
}
/**
@@ -70,19 +81,9 @@ function activateWallet(currentStep, parameters) {
onfidoData = parameters.onfidoData;
Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {error: '', loading: true});
} else if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) {
- setAdditionalDetailsStep(true);
-
- // Personal details are heavily validated on the API side. We will only do a quick check to ensure the values
- // exist in some capacity and then stringify them.
- const errorFields = _.reduce(CONST.WALLET.REQUIRED_ADDITIONAL_DETAILS_FIELDS, (missingFields, fieldName) => (
- !personalDetails[fieldName] ? [...missingFields, fieldName] : missingFields
- ), []);
-
- if (!_.isEmpty(errorFields)) {
- setAdditionalDetailsStep(false, errorFields);
- return;
- }
-
+ setAdditionalDetailsLoading(true);
+ setAdditionalDetailsErrors(null);
+ setAdditionalDetailsErrorMessage('');
personalDetails = JSON.stringify(parameters.personalDetails);
} else if (currentStep === CONST.WALLET.STEP.TERMS) {
hasAcceptedTerms = parameters.hasAcceptedTerms;
@@ -104,7 +105,15 @@ function activateWallet(currentStep, parameters) {
if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) {
if (response.title === CONST.WALLET.ERROR.MISSING_FIELD) {
- setAdditionalDetailsStep(false, response.data.fieldNames);
+ // Errors for missing fields are returned from the API as an array of strings so we are converting this to an
+ // object with field names as keys and boolean for values
+ const errorFields = _.reduce(response.data.fieldNames, (errors, fieldName) => ({
+ ...errors,
+ [fieldName]: true,
+ }), {});
+ setAdditionalDetailsLoading(false);
+ setAdditionalDetailsErrors(errorFields);
+ setAdditionalDetailsErrorMessage('');
return;
}
@@ -116,11 +125,15 @@ function activateWallet(currentStep, parameters) {
];
if (_.contains(errorTitles, response.title)) {
- setAdditionalDetailsStep(false, null, response.message);
+ setAdditionalDetailsLoading(false);
+ setAdditionalDetailsErrorMessage(response.message);
+ setAdditionalDetailsErrors(null);
return;
}
- setAdditionalDetailsStep(false);
+ setAdditionalDetailsLoading(false);
+ setAdditionalDetailsErrors(null);
+ setAdditionalDetailsErrorMessage('');
return;
}
@@ -132,7 +145,9 @@ function activateWallet(currentStep, parameters) {
if (currentStep === CONST.WALLET.STEP.ONFIDO) {
Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {error: '', loading: true});
} else if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) {
- setAdditionalDetailsStep(false);
+ setAdditionalDetailsLoading(false);
+ setAdditionalDetailsErrors(null);
+ setAdditionalDetailsErrorMessage('');
} else if (currentStep === CONST.WALLET.STEP.TERMS) {
Onyx.merge(ONYXKEYS.WALLET_TERMS, {loading: false});
}
@@ -159,9 +174,18 @@ function fetchUserWallet() {
});
}
+/**
+ * @param {Object} keyValuePair
+ */
+function updateAdditionalDetailsDraft(keyValuePair) {
+ Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS_DRAFT, keyValuePair);
+}
+
export {
fetchOnfidoToken,
- setAdditionalDetailsStep,
activateWallet,
fetchUserWallet,
+ setAdditionalDetailsErrors,
+ updateAdditionalDetailsDraft,
+ setAdditionalDetailsErrorMessage,
};
diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js
index 46e1c772dde5..72e39de7e2ce 100644
--- a/src/pages/EnablePayments/AdditionalDetailsStep.js
+++ b/src/pages/EnablePayments/AdditionalDetailsStep.js
@@ -1,16 +1,18 @@
+import lodashGet from 'lodash/get';
+import lodashUnset from 'lodash/unset';
+import lodashCloneDeep from 'lodash/cloneDeep';
import _ from 'underscore';
import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import {
- View, ScrollView, KeyboardAvoidingView,
+ View, KeyboardAvoidingView,
} from 'react-native';
import ScreenWrapper from '../../components/ScreenWrapper';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import Navigation from '../../libs/Navigation/Navigation';
import styles from '../../styles/styles';
-import ExpensifyButton from '../../components/ExpensifyButton';
import ExpensifyText from '../../components/ExpensifyText';
import * as BankAccounts from '../../libs/actions/BankAccounts';
import CONST from '../../CONST';
@@ -18,6 +20,12 @@ import compose from '../../libs/compose';
import ONYXKEYS from '../../ONYXKEYS';
import TextLink from '../../components/TextLink';
import ExpensiTextInput from '../../components/ExpensiTextInput';
+import FormScrollView from '../../components/FormScrollView';
+import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton';
+import * as Wallet from '../../libs/actions/Wallet';
+import * as ValidationUtils from '../../libs/ValidationUtils';
+import AddressSearch from '../../components/AddressSearch';
+import DatePicker from '../../components/DatePicker';
const propTypes = {
...withLocalizePropTypes,
@@ -28,7 +36,7 @@ const propTypes = {
loading: PropTypes.bool,
/** Which field needs attention? */
- errorFields: PropTypes.arrayOf(PropTypes.string),
+ errorFields: PropTypes.objectOf(PropTypes.bool),
/** Any additional error message to show */
additionalErrorMessage: PropTypes.string,
@@ -47,68 +55,123 @@ class AdditionalDetailsStep extends React.Component {
constructor(props) {
super(props);
- this.requiredText = `${props.translate('common.isRequiredField')}.`;
- this.fields = [
- {
- label: props.translate('additionalDetailsStep.legalFirstNameLabel'),
- fieldName: 'legalFirstName',
- },
- {
- label: props.translate('additionalDetailsStep.legalMiddleNameLabel'),
- fieldName: 'legalMiddleName',
- },
- {
- label: props.translate('additionalDetailsStep.legalLastNameLabel'),
- fieldName: 'legalLastName',
- },
- {
- label: props.translate('common.personalAddress'),
- fieldName: 'addressStreet',
- },
- {
- label: props.translate('common.city'),
- fieldName: 'addressCity',
- },
- {
- label: props.translate('common.state'),
- fieldName: 'addressState',
- },
- {
- label: props.translate('common.zip'),
- fieldName: 'addressZip',
- },
- {
- label: props.translate('common.phoneNumber'),
- fieldName: 'phoneNumber',
- },
- {
- label: props.translate('common.dob'),
- fieldName: 'dob',
- },
- {
- label: props.translate('common.ssnLast4'),
- fieldName: 'ssn',
- maxLength: 4,
- keyboardType: CONST.KEYBOARD_TYPE.NUMBER_PAD,
- },
+ this.activateWallet = this.activateWallet.bind(this);
+
+ this.requiredFields = [
+ 'legalFirstName',
+ 'legalLastName',
+ 'addressStreet',
+ 'addressCity',
+ 'addressState',
+ 'addressZip',
+ 'phoneNumber',
+ 'dob',
+ 'ssn',
];
+ this.fieldNameTranslationKeys = {
+ legalFirstName: 'additionalDetailsStep.legalFirstNameLabel',
+ legalMiddleName: 'additionalDetailsStep.legalMiddleNameLabel',
+ legalLastName: 'additionalDetailsStep.legalLastNameLabel',
+ addressStreet: 'common.personalAddress',
+ addressCity: 'common.city',
+ addressState: 'common.state',
+ addressZip: 'common.zip',
+ phoneNumber: 'common.phoneNumber',
+ dob: 'common.dob',
+ ssn: 'common.ssnLast4',
+ };
+
this.state = {
- firstName: '',
- legalMiddleName: '',
- legalLastName: '',
- address: '',
- city: '',
- state: '',
- zipCode: '',
- phoneNumber: '',
- dob: '',
- ssn: '',
+ legalFirstName: lodashGet(props.walletAdditionalDetailsDraft, 'legalFirstName', ''),
+ legalMiddleName: lodashGet(props.walletAdditionalDetailsDraft, 'legalMiddleName', ''),
+ legalLastName: lodashGet(props.walletAdditionalDetailsDraft, 'legalLastName', ''),
+ addressStreet: lodashGet(props.walletAdditionalDetailsDraft, 'addressStreet', ''),
+ addressCity: lodashGet(props.walletAdditionalDetailsDraft, 'addressCity', ''),
+ addressState: lodashGet(props.walletAdditionalDetailsDraft, 'addressState', ''),
+ addressZip: lodashGet(props.walletAdditionalDetailsDraft, 'addressZip', ''),
+ phoneNumber: lodashGet(props.walletAdditionalDetailsDraft, 'phoneNumber', ''),
+ dob: lodashGet(props.walletAdditionalDetailsDraft, 'dob', ''),
+ ssn: lodashGet(props.walletAdditionalDetailsDraft, 'ssn', ''),
};
}
+ /**
+ * @param {String} fieldName
+ * @returns {String}
+ */
+ getErrorText(fieldName) {
+ const errors = lodashGet(this.props.walletAdditionalDetails, 'errorFields', {});
+ if (!errors[fieldName]) {
+ return '';
+ }
+
+ return `${this.props.translate(this.fieldNameTranslationKeys[fieldName])} ${this.props.translate('common.isRequiredField')}.`;
+ }
+
+ /**
+ * @returns {Boolean}
+ */
+ validate() {
+ // Reset server error messages when resubmitting form
+ Wallet.setAdditionalDetailsErrorMessage('');
+
+ const errors = {};
+
+ if (!ValidationUtils.isValidPastDate(this.state.dob)) {
+ errors.dob = true;
+ }
+
+ if (!ValidationUtils.isValidAddress(this.state.addressStreet)) {
+ errors.addressStreet = true;
+ }
+
+ if (!ValidationUtils.isValidSSNLastFour(this.state.ssn)) {
+ errors.ssn = true;
+ }
+
+ _.each(this.requiredFields, (requiredField) => {
+ if (ValidationUtils.isRequiredFulfilled(this.state[requiredField])) {
+ return;
+ }
+
+ errors[requiredField] = true;
+ });
+
+ Wallet.setAdditionalDetailsErrors(errors);
+ return _.size(errors) === 0;
+ }
+
+ activateWallet() {
+ if (!this.validate()) {
+ return;
+ }
+
+ BankAccounts.activateWallet(CONST.WALLET.STEP.ADDITIONAL_DETAILS, {
+ personalDetails: this.state,
+ });
+ }
+
+ /**
+ * @param {String} fieldName
+ * @param {String} value
+ */
+ clearErrorAndSetValue(fieldName, value) {
+ this.setState({[fieldName]: value});
+ Wallet.updateAdditionalDetailsDraft({[fieldName]: value});
+ const errors = lodashGet(this.props, 'walletAdditionalDetails.errorFields', {});
+ if (!lodashGet(errors, fieldName, false)) {
+ return;
+ }
+
+ const newErrors = lodashCloneDeep(errors);
+ lodashUnset(newErrors, fieldName);
+ Wallet.setAdditionalDetailsErrors(newErrors);
+ }
+
render() {
- const errorFields = this.props.walletAdditionalDetails.errorFields || [];
+ const isErrorVisible = _.size(lodashGet(this.props, 'walletAdditionalDetails.errorFields', {})) > 0
+ || lodashGet(this.props, 'walletAdditionalDetails.additionalErrorMessage', '').length > 0;
return (
@@ -126,41 +189,83 @@ class AdditionalDetailsStep extends React.Component {
{this.props.translate('additionalDetailsStep.helpLink')}
-
- {_.map(this.fields, field => (
- <>
- this.setState({[field.fieldName]: val})}
- value={this.state[field.fieldName]}
- errorText={_.contains(errorFields, field.fieldName)
- ? `${field.label} ${this.requiredText}`
- : ''}
- // eslint-disable-next-line react/jsx-props-no-spreading
- {..._.omit(field, ['label', 'fieldName'])}
+ this.form = el}>
+
+ this.clearErrorAndSetValue('legalFirstName', val)}
+ value={this.state.legalFirstName}
+ errorText={this.getErrorText('legalFirstName')}
+ />
+ this.clearErrorAndSetValue('legalMiddleName', val)}
+ value={this.state.legalMiddleName}
+ />
+ this.clearErrorAndSetValue('legalLastName', val)}
+ value={this.state.legalLastName}
+ errorText={this.getErrorText('legalLastName')}
+ />
+
+ {
+ if (fieldName === 'addressZipCode') {
+ this.clearErrorAndSetValue('addressZip', value);
+ return;
+ }
+
+ this.clearErrorAndSetValue(fieldName, value);
+ }}
+ errorText={this.getErrorText('addressStreet')}
/>
- {field.fieldName === 'addressStreet' && {this.props.translate('common.noPO')}}
- >
- ))}
-
-
- {this.props.walletAdditionalDetails.additionalErrorMessage.length > 0 && (
-
- {this.props.walletAdditionalDetails.additionalErrorMessage}
-
- )}
- {
- BankAccounts.activateWallet(CONST.WALLET.STEP.ADDITIONAL_DETAILS, {
- personalDetails: this.state,
- });
+
+ {this.props.translate('common.noPO')}
+
+
+ this.clearErrorAndSetValue('phoneNumber', val)}
+ value={this.state.phoneNumber}
+ errorText={this.getErrorText('phoneNumber')}
+ />
+ this.clearErrorAndSetValue('dob', val)}
+ value={this.state.dob}
+ placeholder={this.props.translate('common.dob')}
+ errorText={this.getErrorText('dob')}
+ maximumDate={new Date()}
+ />
+ this.clearErrorAndSetValue('ssn', val)}
+ value={this.state.ssn}
+ errorText={this.getErrorText('ssn')}
+ maxLength={4}
+ keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
+ />
+
+ {
+ this.form.scrollTo({y: 0, animated: true});
}}
+ message={this.props.walletAdditionalDetails.additionalErrorMessage}
+ isLoading={this.props.walletAdditionalDetails.loading}
+ buttonText={this.props.translate('common.saveAndContinue')}
/>
-
+
@@ -177,5 +282,8 @@ export default compose(
key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS,
initWithStoredValues: false,
},
+ walletAdditionalDetailsDraft: {
+ key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS_DRAFT,
+ },
}),
)(AdditionalDetailsStep);