diff --git a/src/CONST.js b/src/CONST.js
index 1c65f2924487..5c1fd52f8798 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -428,11 +428,11 @@ const CONST = {
},
ERROR: {
FULL_SSN_NOT_FOUND: 'Full SSN not found',
- IDENTITY_NOT_FOUND: 'Identity not found',
- INVALID_SSN: 'Invalid SSN',
- UNEXPECTED: 'Unexpected error',
MISSING_FIELD: 'Missing required additional details fields',
- UNABLE_TO_VERIFY: 'Unable to verify identity',
+ WRONG_ANSWERS: 'Wrong answers',
+
+ // KBA stands for Knowledge Based Answers (requiring us to show Idology questions)
+ KBA_NEEDED: 'KBA needed',
NO_ACCOUNT_TO_LINK: '405 No account to link to wallet',
INVALID_WALLET: '405 Invalid wallet account',
NOT_OWNER_OF_BANK_ACCOUNT: '401 Wallet owner does not own linked bank account',
@@ -441,6 +441,7 @@ const CONST = {
INVALID_FUND: '405 Attempting to link an invalid fund to a wallet',
},
STEP: {
+ // In the order they appear in the Wallet flow
ONFIDO: 'OnfidoStep',
ADDITIONAL_DETAILS: 'AdditionalDetailsStep',
TERMS: 'TermsStep',
diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js
index fcfc43376e3a..8dafcca121ef 100644
--- a/src/components/AddressSearch.js
+++ b/src/components/AddressSearch.js
@@ -91,8 +91,10 @@ const AddressSearch = (props) => {
const zipCode = GooglePlacesUtils.getAddressComponent(addressComponents, 'postal_code', 'long_name');
const state = GooglePlacesUtils.getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name');
- const values = {};
- if (street && props.value && street.length > props.value.trim().length) {
+ const values = {
+ street: props.value ? props.value.trim() : '',
+ };
+ if (street && street.length >= values.street.length) {
// We are only passing the street number and name if the combined length is longer than the value
// that was initially passed to the autocomplete component. Google Places can truncate details
// like Apt # and this is the best way we have to tell that the new value it's giving us is less
@@ -158,6 +160,7 @@ const AddressSearch = (props) => {
inputID: props.inputID,
shouldSaveDraft: props.shouldSaveDraft,
onBlur: props.onBlur,
+ autoComplete: 'none',
onChangeText: (text) => {
if (skippedFirstOnChangeTextRef.current) {
props.onChange({street: text});
diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js
index a8a9697632e9..a324c25ae878 100644
--- a/src/components/DatePicker/index.js
+++ b/src/components/DatePicker/index.js
@@ -48,8 +48,7 @@ class Datepicker extends React.Component {
const asMoment = moment(text);
if (asMoment.isValid()) {
- const asDate = asMoment.toDate();
- this.props.onChange(asDate);
+ this.props.onChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING));
}
}
diff --git a/src/languages/en.js b/src/languages/en.js
index 56aab993560f..a17924c1f7dc 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -98,6 +98,7 @@ export default {
enterManually: 'Enter it manually',
message: 'Message ',
leaveRoom: 'Leave room',
+ your: 'your',
conciergeHelp: 'Please reach out to Concierge for help.',
},
attachmentPicker: {
@@ -570,10 +571,15 @@ export default {
additionalDetailsStep: {
headerTitle: 'Additional details',
helpText: 'We need to confirm the following information before we can process this payment.',
+ helpTextIdologyQuestions: 'We need to ask you just a few more questions to finish validating your identity.',
helpLink: 'Learn more about why we need this.',
legalFirstNameLabel: 'Legal first name',
legalMiddleNameLabel: 'Legal middle name',
legalLastNameLabel: 'Legal last name',
+ selectAnswer: 'You need to select a response to proceed.',
+ needSSNFull9: 'We\'re having trouble verifying your SSN. Please enter the full 9 digits of your SSN.',
+ weCouldNotVerify: 'We could not verify',
+ pleaseFixIt: 'Please fix this information before continuing.',
failedKYCTextBefore: 'We weren\'t able to successfully verify your identity. Please try again later and reach out to ',
failedKYCTextAfter: ' if you have any questions.',
},
diff --git a/src/languages/es.js b/src/languages/es.js
index d6b40f3495c5..a247568bb3e5 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -98,6 +98,7 @@ export default {
enterManually: 'Ingresar manualmente',
message: 'Chatear con ',
leaveRoom: 'Salir de la sala de chat',
+ your: 'tu',
conciergeHelp: 'Por favor contacta con Concierge para obtener ayuda.',
},
attachmentPicker: {
@@ -570,10 +571,15 @@ export default {
additionalDetailsStep: {
headerTitle: 'Detalles adicionales',
helpText: 'Necesitamos confirmar la siguiente información antes de que podamos procesar este pago.',
+ helpTextIdologyQuestions: 'Tenemos que preguntarte unas preguntas mas para terminar de verificar tu identidad',
helpLink: 'Obtenga más información sobre por qué necesitamos esto.',
legalFirstNameLabel: 'Primer nombre legal',
legalMiddleNameLabel: 'Segundo nombre legal',
legalLastNameLabel: 'Apellido legal',
+ selectAnswer: 'Selecciona una respuesta.',
+ needSSNFull9: 'Estamos teniendo problemas para verificar su SSN. Ingresa los 9 dígitos del SSN.',
+ weCouldNotVerify: 'No pudimos verificar',
+ pleaseFixIt: 'Corrije esta información antes de continuar.',
failedKYCTextBefore: 'No pudimos verificar correctamente su identidad. Vuelva a intentarlo más tarde y comuníquese con ',
failedKYCTextAfter: ' si tiene alguna pregunta.',
},
diff --git a/src/libs/API.js b/src/libs/API.js
index 1dfbd2f5ed15..1fbfe935e5bf 100644
--- a/src/libs/API.js
+++ b/src/libs/API.js
@@ -921,6 +921,7 @@ function Plaid_GetLinkToken() {
* @param {String} parameters.currentStep
* @param {String} [parameters.onfidoData] - JSON string
* @param {String} [parameters.personalDetails] - JSON string
+ * @param {String} [parameters.idologyAnswers] - JSON string
* @param {Boolean} [parameters.hasAcceptedTerms]
* @returns {Promise}
*/
diff --git a/src/libs/Localize.js b/src/libs/Localize.js
index 3fd7ca2253f5..dad089feff79 100644
--- a/src/libs/Localize.js
+++ b/src/libs/Localize.js
@@ -78,7 +78,27 @@ function translateLocal(phrase, variables) {
return translate(preferredLocale, phrase, variables);
}
+/**
+ * Format an array into a string with comma and "and" ("a dog, a cat and a chicken")
+ *
+ * @param {Array} anArray
+ * @return {String}
+ */
+function arrayToString(anArray) {
+ const and = this.translateLocal('common.and');
+ let aString = '';
+ if (_.size(anArray) === 1) {
+ aString = anArray[0];
+ } else if (_.size(anArray) === 2) {
+ aString = anArray.join(` ${and} `);
+ } else if (_.size(anArray) > 2) {
+ aString = `${anArray.slice(0, -1).join(', ')} ${and} ${anArray.slice(-1)}`;
+ }
+ return aString;
+}
+
export {
translate,
translateLocal,
+ arrayToString,
};
diff --git a/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js b/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js
index fbd85d89ee31..c3c713c512c6 100644
--- a/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js
+++ b/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js
@@ -231,7 +231,7 @@ function setupWithdrawalAccount(params) {
// Example 1: When forcing manual step after adding Chase bank account via Plaid, so we can ask for the real numbers instead of the plaid substitutes
// Example 2: When on the requestor step, showing Onfido view after submitting the identity and retrieving the sdkToken
if (_.has(responseACHData, 'nextStepValues')) {
- navigation.goToWithdrawalAccountSetupStep(_.get(responseACHData.nextStepValues, 'currentStep') || nextStep, {
+ navigation.goToWithdrawalAccountSetupStep(lodashGet(responseACHData, 'nextStepValues.currentStep') || nextStep, {
...updatedACHData,
...(_.omit(responseACHData, 'nextStepValues')),
...responseACHData.nextStepValues,
diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js
index a691f0a09170..05fb2e8dc7b8 100644
--- a/src/libs/actions/Wallet.js
+++ b/src/libs/actions/Wallet.js
@@ -1,10 +1,12 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
+import Str from 'expensify-common/lib/str';
import Onyx from 'react-native-onyx';
import ONYXKEYS from '../../ONYXKEYS';
import * as API from '../API';
import CONST from '../../CONST';
import * as PaymentMethods from './PaymentMethods';
+import * as Localize from '../Localize';
/**
* Fetch and save locally the Onfido SDK token and applicantID
@@ -36,6 +38,14 @@ function setAdditionalDetailsLoading(loading) {
Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {loading});
}
+/**
+ * @param {Array} questions
+ * @param {String} [idNumber]
+ */
+function setAdditionalDetailsQuestions(questions, idNumber) {
+ Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {questions, idNumber});
+}
+
/**
* @param {Object} errorFields
*/
@@ -58,6 +68,84 @@ function setAdditionalDetailsShouldAskForFullSSN(shouldAskForFullSSN) {
Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {shouldAskForFullSSN});
}
+/**
+ * @param {Boolean} shouldShowFailedKYC
+ */
+function setAdditionalDetailsShouldShowFailedKYC(shouldShowFailedKYC) {
+ Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {shouldShowFailedKYC});
+}
+
+/**
+ * Transforms a list of Idology errors to a translated displayable error string.
+ * @param {Array} idologyErrors
+ * @return {String}
+ */
+function buildIdologyError(idologyErrors) {
+ if (_.isEmpty(idologyErrors)) {
+ return '';
+ }
+ const addressErrors = [
+ 'resultcode.address.does.not.match',
+ 'resultcode.street.name.does.not.match',
+ 'resultcode.street.number.does.not.match',
+ 'resultcode.zip.does.not.match',
+ 'resultcode.alternate.address.alert',
+ 'resultcode.state.does.not.match',
+ 'resultcode.input.address.is.po.box',
+ 'resultcode.located.address.is.po.box',
+ 'resultcode.warm.address.alert',
+ ];
+ const dobErrors = [
+ 'resultcode.coppa.alert',
+ 'resultcode.age.below.minimum',
+ 'resultcode.dob.does.not.match',
+ 'resultcode.yob.does.not.match',
+ 'resultcode.yob.within.one.year',
+ 'resultcode.mob.does.not.match',
+ 'resultcode.no.mob.available',
+ 'resultcode.no.dob.available',
+ 'resultcode.ssn.issued.prior.to.dob',
+ ];
+ const ssnErrors = [
+ 'resultcode.ssn.does.not.match',
+ 'resultcode.ssn.within.one.digit',
+ 'resultcode.ssn.not.valid',
+ 'resultcode.ssn.issued.prior.to.dob',
+ 'resultcode.input.ssn.is.itin',
+ 'resultcode.located.itin',
+ ];
+ const nameErrors = [
+ 'resultcode.last.name.does.not.match',
+ ];
+
+ // List of translated errors
+ const errorsTranslated = _.uniq(_.reduce(idologyErrors, (memo, error) => {
+ const your = Localize.translateLocal('common.your');
+ if (_.contains(addressErrors, error)) {
+ memo.push(`${your} ${Localize.translateLocal('common.personalAddress').toLowerCase()}`);
+ }
+ if (_.contains(dobErrors, error)) {
+ memo.push(`${your} ${Localize.translateLocal('common.dob').toLowerCase()}`);
+ }
+ if (_.contains(ssnErrors, error)) {
+ memo.push(`${your} SSN`);
+ }
+ if (_.contains(nameErrors, error)) {
+ memo.push(`${your} ${Localize.translateLocal('additionalDetailsStep.legalLastNameLabel').toLowerCase()}`);
+ }
+
+ return memo;
+ }, []));
+
+ if (_.isEmpty(errorsTranslated)) {
+ return '';
+ }
+
+ const errorStart = Localize.translateLocal('additionalDetailsStep.weCouldNotVerify');
+ const errorEnd = Localize.translateLocal('additionalDetailsStep.pleaseFixIt');
+ return `${errorStart} ${Localize.arrayToString(errorsTranslated)}. ${errorEnd}`;
+}
+
/**
* This action can be called repeatedly with different steps until an Expensify Wallet has been activated.
*
@@ -73,11 +161,13 @@ function setAdditionalDetailsShouldAskForFullSSN(shouldAskForFullSSN) {
* @param {String} currentStep
* @param {Object} parameters
* @param {String} [parameters.onfidoData] - JSON string
- * @param {Object} [parameters.personalDetails] - JSON string
+ * @param {Object} [parameters.personalDetails]
+ * @param {Object} [parameters.idologyAnswers]
* @param {Boolean} [parameters.hasAcceptedTerms]
*/
function activateWallet(currentStep, parameters) {
let personalDetails;
+ let idologyAnswers;
let onfidoData;
let hasAcceptedTerms;
@@ -89,10 +179,16 @@ function activateWallet(currentStep, parameters) {
onfidoData = parameters.onfidoData;
Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {error: '', loading: true});
} else if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) {
- setAdditionalDetailsLoading(true);
- setAdditionalDetailsErrors(null);
- setAdditionalDetailsErrorMessage('');
- personalDetails = JSON.stringify(parameters.personalDetails);
+ if (parameters.personalDetails) {
+ setAdditionalDetailsLoading(true);
+ setAdditionalDetailsErrors(null);
+ setAdditionalDetailsErrorMessage('');
+ setAdditionalDetailsShouldShowFailedKYC(false);
+ personalDetails = JSON.stringify(parameters.personalDetails);
+ }
+ if (parameters.idologyAnswers) {
+ idologyAnswers = JSON.stringify(parameters.idologyAnswers);
+ }
} else if (currentStep === CONST.WALLET.STEP.TERMS) {
hasAcceptedTerms = parameters.hasAcceptedTerms;
Onyx.merge(ONYXKEYS.WALLET_TERMS, {loading: true});
@@ -101,10 +197,19 @@ function activateWallet(currentStep, parameters) {
API.Wallet_Activate({
currentStep,
personalDetails,
+ idologyAnswers,
onfidoData,
hasAcceptedTerms,
})
.then((response) => {
+ if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) {
+ // Hide the loader
+ setAdditionalDetailsLoading(false);
+
+ // Make sure we remove any questions from Onyx once we've answered them
+ setAdditionalDetailsQuestions(null);
+ }
+
if (response.jsonCode !== 200) {
if (currentStep === CONST.WALLET.STEP.ONFIDO) {
Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {error: response.message, loading: false});
@@ -112,8 +217,9 @@ function activateWallet(currentStep, parameters) {
}
if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) {
- // Hide the loader
- setAdditionalDetailsLoading(false);
+ if (response.title === CONST.WALLET.ERROR.KBA_NEEDED) {
+ setAdditionalDetailsQuestions(response.data.questions, response.data.idNumber);
+ }
if (response.title === CONST.WALLET.ERROR.MISSING_FIELD) {
// Convert array of strings to object with field names as keys and boolean for values (true if error, false if not)
@@ -126,17 +232,48 @@ function activateWallet(currentStep, parameters) {
if (response.title === CONST.WALLET.ERROR.FULL_SSN_NOT_FOUND) {
setAdditionalDetailsShouldAskForFullSSN(true);
+ setAdditionalDetailsErrorMessage(Localize.translateLocal('additionalDetailsStep.needSSNFull9'));
+ return;
}
- const errorTitles = [
- CONST.WALLET.ERROR.FULL_SSN_NOT_FOUND,
- CONST.WALLET.ERROR.IDENTITY_NOT_FOUND,
- CONST.WALLET.ERROR.INVALID_SSN,
- CONST.WALLET.ERROR.UNEXPECTED,
- CONST.WALLET.ERROR.UNABLE_TO_VERIFY,
- ];
+ let qualifiers = lodashGet(response, 'data.requestorIdentityID.apiResult.qualifiers.qualifier', []);
- if (_.contains(errorTitles, response.title)) {
+ // ExpectID sometimes returns qualifier as an object when there is only one, or as an array if there are several
+ if (qualifiers.key) {
+ qualifiers = [qualifiers];
+ }
+ const idologyErrors = _.map(qualifiers, error => error.key);
+
+ if (!_.isEmpty(idologyErrors)) {
+ // These errors should redirect to the KYC failure page
+ const hardFailures = [
+ 'resultcode.newer.record.found',
+ 'resultcode.high.risk.address.alert',
+ 'resultcode.ssn.not.available',
+ 'resultcode.subject.deceased',
+ 'resultcode.thin.file',
+ 'resultcode.pa.dob.match',
+ 'resultcode.pa.dob.not.available',
+ 'resultcode.pa.dob.does.not.match',
+ ];
+ if (_.some(hardFailures, hardFailure => _.contains(idologyErrors, hardFailure))) {
+ setAdditionalDetailsShouldShowFailedKYC(true);
+ return;
+ }
+
+ const identityError = buildIdologyError(idologyErrors);
+ if (identityError) {
+ setAdditionalDetailsErrorMessage(identityError);
+ return;
+ }
+ }
+
+ if (lodashGet(response, 'data.requestorIdentityID.apiResult.results.key') === 'result.no.match'
+ || response.title === CONST.WALLET.ERROR.WRONG_ANSWERS) {
+ setAdditionalDetailsShouldShowFailedKYC(true);
+ return;
+ }
+ if (Str.endsWith(response.type, 'AutoVerifyFailure')) {
setAdditionalDetailsErrorMessage(response.message);
}
@@ -199,4 +336,6 @@ export {
setAdditionalDetailsErrors,
updateAdditionalDetailsDraft,
setAdditionalDetailsErrorMessage,
+ setAdditionalDetailsQuestions,
+ buildIdologyError,
};
diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js
index a15d70ddd3ed..1fc82c82354e 100644
--- a/src/pages/EnablePayments/AdditionalDetailsStep.js
+++ b/src/pages/EnablePayments/AdditionalDetailsStep.js
@@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx';
import {
View, KeyboardAvoidingView,
} from 'react-native';
+import IdologyQuestions from './IdologyQuestions';
import ScreenWrapper from '../../components/ScreenWrapper';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
@@ -25,6 +26,7 @@ import * as ValidationUtils from '../../libs/ValidationUtils';
import AddressSearch from '../../components/AddressSearch';
import DatePicker from '../../components/DatePicker';
import FormHelper from '../../libs/FormHelper';
+import FailedKYC from './FailedKYC';
const propTypes = {
...withLocalizePropTypes,
@@ -39,6 +41,35 @@ const propTypes = {
/** Any additional error message to show */
additionalErrorMessage: PropTypes.string,
+
+ /** Questions returned by Idology */
+ questions: PropTypes.arrayOf(PropTypes.shape({
+ prompt: PropTypes.string,
+ type: PropTypes.string,
+ answer: PropTypes.arrayOf(PropTypes.string),
+ })),
+
+ /** ExpectID ID number related to those questions */
+ idNumber: PropTypes.string,
+
+ /** If we should show the FailedKYC view after the user submitted the form with a non fixable error */
+ shouldShowFailedKYC: PropTypes.bool,
+
+ /** If we should ask for the full SSN (when LexisNexis failed retrieving the first 5 from the last 4) */
+ shouldAskForFullSSN: PropTypes.bool,
+ }),
+
+ /** Stores the personal details typed by the user */
+ walletAdditionalDetailsDraft: PropTypes.shape({
+ legalFirstName: PropTypes.string,
+ legalLastName: PropTypes.string,
+ addressStreet: PropTypes.string,
+ addressCity: PropTypes.string,
+ addressState: PropTypes.string,
+ addressZip: PropTypes.string,
+ phoneNumber: PropTypes.string,
+ dob: PropTypes.string,
+ ssn: PropTypes.string,
}),
};
@@ -47,6 +78,21 @@ const defaultProps = {
errorFields: {},
loading: false,
additionalErrorMessage: '',
+ questions: [],
+ idNumber: '',
+ shouldShowFailedKYC: false,
+ shouldAskForFullSSN: false,
+ },
+ walletAdditionalDetailsDraft: {
+ legalFirstName: '',
+ legalLastName: '',
+ addressStreet: '',
+ addressCity: '',
+ addressState: '',
+ addressZip: '',
+ phoneNumber: '',
+ dob: '',
+ ssn: '',
},
};
@@ -81,18 +127,6 @@ class AdditionalDetailsStep extends React.Component {
ssnFull9: 'common.ssnFull9',
};
- this.state = {
- legalFirstName: lodashGet(props.walletAdditionalDetailsDraft, 'legalFirstName', ''),
- 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', ''),
- };
-
this.formHelper = new FormHelper({
errorPath: 'walletAdditionalDetails.errorFields',
setErrors: Wallet.setAdditionalDetailsErrors,
@@ -134,20 +168,20 @@ class AdditionalDetailsStep extends React.Component {
const errors = {};
- if (!ValidationUtils.isValidPastDate(this.state.dob)) {
+ if (!ValidationUtils.isValidPastDate(this.props.walletAdditionalDetailsDraft.dob)) {
errors.dob = true;
}
- if (!ValidationUtils.isValidAddress(this.state.addressStreet)) {
+ if (!ValidationUtils.isValidAddress(this.props.walletAdditionalDetailsDraft.addressStreet)) {
errors.addressStreet = true;
}
- if (!ValidationUtils.isValidSSNLastFour(this.state.ssn) && !ValidationUtils.isValidSSNFullNine(this.state.ssn)) {
+ if (!ValidationUtils.isValidSSNLastFour(this.props.walletAdditionalDetailsDraft.ssn) && !ValidationUtils.isValidSSNFullNine(this.props.walletAdditionalDetailsDraft.ssn)) {
errors.ssn = true;
}
_.each(this.requiredFields, (requiredField) => {
- if (ValidationUtils.isRequiredFulfilled(this.state[requiredField])) {
+ if (ValidationUtils.isRequiredFulfilled(this.props.walletAdditionalDetailsDraft[requiredField])) {
return;
}
@@ -164,7 +198,7 @@ class AdditionalDetailsStep extends React.Component {
}
BankAccounts.activateWallet(CONST.WALLET.STEP.ADDITIONAL_DETAILS, {
- personalDetails: this.state,
+ personalDetails: this.props.walletAdditionalDetailsDraft,
});
}
@@ -173,12 +207,44 @@ class AdditionalDetailsStep extends React.Component {
* @param {String} value
*/
clearErrorAndSetValue(fieldName, value) {
- this.setState({[fieldName]: value});
Wallet.updateAdditionalDetailsDraft({[fieldName]: value});
this.clearError(fieldName);
}
render() {
+ if (this.props.walletAdditionalDetails.shouldShowFailedKYC) {
+ return (
+
+
+ Navigation.dismissModal()}
+ />
+
+
+
+ );
+ }
+
+ if (!_.isEmpty(this.props.walletAdditionalDetails.questions)) {
+ return (
+
+
+ Navigation.dismissModal()}
+ shouldShowBackButton
+ onBackButtonPress={() => Wallet.setAdditionalDetailsQuestions(null)}
+ />
+
+
+
+ );
+ }
+
const isErrorVisible = _.size(this.getErrors()) > 0
|| lodashGet(this.props, 'walletAdditionalDetails.additionalErrorMessage', '').length > 0;
const shouldAskForFullSSN = this.props.walletAdditionalDetails.shouldAskForFullSSN;
@@ -202,24 +268,25 @@ class AdditionalDetailsStep extends React.Component {
this.form = el}>
- this.clearErrorAndSetValue('legalFirstName', val)}
- value={this.state.legalFirstName}
- errorText={this.getErrorText('legalFirstName')}
- />
- this.clearErrorAndSetValue('legalLastName', val)}
- value={this.state.legalLastName}
- errorText={this.getErrorText('legalLastName')}
- />
+ this.clearErrorAndSetValue('legalFirstName', val)}
+ value={this.props.walletAdditionalDetailsDraft.legalFirstName || ''}
+ errorText={this.getErrorText('legalFirstName')}
+ />
+ this.clearErrorAndSetValue('legalLastName', val)}
+ value={this.props.walletAdditionalDetailsDraft.legalLastName || ''}
+ errorText={this.getErrorText('legalLastName')}
+ />
{
const renamedFields = {
street: 'addressStreet',
@@ -237,19 +304,46 @@ class AdditionalDetailsStep extends React.Component {
{this.props.translate('common.noPO')}
+ {this.props.walletAdditionalDetailsDraft.addressStreet ? (
+ <>
+ {/** Once the user has started entering his address, show the other address fields (city, state, zip) */}
+ {/** We'll autofill them when the user selects a full address from the google autocomplete */}
+ this.clearErrorAndSetValue('addressCity', val)}
+ value={this.props.walletAdditionalDetailsDraft.addressCity || ''}
+ errorText={this.getErrorText('addressCity')}
+ />
+ this.clearErrorAndSetValue('addressState', val)}
+ value={this.props.walletAdditionalDetailsDraft.addressState || ''}
+ errorText={this.getErrorText('addressState')}
+ />
+ this.clearErrorAndSetValue('addressZip', val)}
+ value={this.props.walletAdditionalDetailsDraft.addressZip || ''}
+ errorText={this.getErrorText('addressZip')}
+ />
+ >
+ ) : null}
this.clearErrorAndSetValue('phoneNumber', val)}
- value={this.state.phoneNumber}
+ value={this.props.walletAdditionalDetailsDraft.phoneNumber || ''}
errorText={this.getErrorText('phoneNumber')}
/>
this.clearErrorAndSetValue('dob', val)}
- value={this.state.dob}
+ value={this.props.walletAdditionalDetailsDraft.dob || ''}
placeholder={this.props.translate('common.dob')}
errorText={this.getErrorText('dob')}
maximumDate={new Date()}
@@ -258,7 +352,7 @@ class AdditionalDetailsStep extends React.Component {
containerStyles={[styles.mt4]}
label={this.props.translate(this.fieldNameTranslationKeys[shouldAskForFullSSN ? 'ssnFull9' : 'ssn'])}
onChangeText={val => this.clearErrorAndSetValue('ssn', val)}
- value={this.state.ssn}
+ value={this.props.walletAdditionalDetailsDraft.ssn || ''}
errorText={this.getErrorText('ssn')}
maxLength={shouldAskForFullSSN ? 9 : 4}
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js
new file mode 100644
index 000000000000..cd8ff54411bd
--- /dev/null
+++ b/src/pages/EnablePayments/IdologyQuestions.js
@@ -0,0 +1,175 @@
+import _ from 'underscore';
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ View,
+} from 'react-native';
+import RadioButtons from '../../components/RadioButtons';
+import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import styles from '../../styles/styles';
+import * as BankAccounts from '../../libs/actions/BankAccounts';
+import CONST from '../../CONST';
+import Text from '../../components/Text';
+import TextLink from '../../components/TextLink';
+import FormScrollView from '../../components/FormScrollView';
+import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton';
+
+const MAX_SKIP = 1;
+const SKIP_QUESTION_TEXT = 'Skip Question';
+
+const propTypes = {
+ ...withLocalizePropTypes,
+
+ /** Questions returned by Idology */
+ /** example: [{"answer":["1251","6253","113","None of the above","Skip Question"],"prompt":"Which number goes with your address on MASONIC AVE?","type":"street.number.b"}, ...] */
+ questions: PropTypes.arrayOf(PropTypes.shape({
+ prompt: PropTypes.string,
+ type: PropTypes.string,
+ answer: PropTypes.arrayOf(PropTypes.string),
+ })),
+
+ /** ID from Idology, referencing those questions */
+ idNumber: PropTypes.string,
+};
+
+const defaultProps = {
+ questions: [],
+ idNumber: '',
+};
+
+class IdologyQuestions extends React.Component {
+ constructor(props) {
+ super(props);
+ this.submitAnswers = this.submitAnswers.bind(this);
+
+ this.state = {
+ /** Current question index to display. */
+ questionNumber: 0,
+
+ /** Should we hide the "Skip question" answer? Yes if the user already skipped MAX_SKIP questions. */
+ hideSkip: false,
+
+ /** Answers from the user */
+ answers: [],
+
+ /** Any error message */
+ errorMessage: '',
+
+ /** Did the user just submitted all his answers? */
+ isLoading: false,
+ };
+ }
+
+ /**
+ * Put question answer in the state.
+ * @param {Number} questionIndex
+ * @param {String} answer
+ */
+ chooseAnswer(questionIndex, answer) {
+ this.setState((prevState) => {
+ const answers = prevState.answers;
+ const question = this.props.questions[questionIndex];
+ answers[questionIndex] = {question: question.type, answer};
+ return {
+ answers,
+ errorMessage: '',
+ };
+ });
+ }
+
+ /**
+ * Show next question or send all answers for Idology verifications when we've answered enough
+ */
+ submitAnswers() {
+ this.setState((prevState) => {
+ // User must pick an answer
+ if (!prevState.answers[prevState.questionNumber]) {
+ return {
+ errorMessage: this.props.translate('additionalDetailsStep.selectAnswer'),
+ };
+ }
+
+ // Get the number of questions that were skipped by the user.
+ const skippedQuestionsCount = _.filter(prevState.answers, answer => answer.answer === SKIP_QUESTION_TEXT).length;
+
+ // We have enough answers, let's call expectID KBA to verify them
+ if ((prevState.answers.length - skippedQuestionsCount) >= (this.props.questions.length - MAX_SKIP)) {
+ const answers = prevState.answers;
+
+ // Auto skip any remaining questions
+ if (answers.length < this.props.questions.length) {
+ for (let i = answers.length; i < this.props.questions.length; i++) {
+ answers[i] = {question: this.props.questions[i].type, answer: SKIP_QUESTION_TEXT};
+ }
+ }
+
+ BankAccounts.activateWallet(CONST.WALLET.STEP.ADDITIONAL_DETAILS, {
+ idologyAnswers: {
+ answers,
+ idNumber: this.props.idNumber,
+ },
+ });
+ return {answers, isLoading: true};
+ }
+
+ // Else, show next question
+ return {
+ questionNumber: prevState.questionNumber + 1,
+ hideSkip: skippedQuestionsCount >= MAX_SKIP,
+ };
+ });
+ }
+
+ render() {
+ const questionIndex = this.state.questionNumber;
+ const question = this.props.questions[questionIndex] || {};
+ const possibleAnswers = _.filter(_.map(question.answer, (answer) => {
+ if (this.state.hideSkip && answer === SKIP_QUESTION_TEXT) {
+ return;
+ }
+
+ return {
+ label: answer,
+ value: answer,
+ };
+ }));
+
+ return (
+
+
+ {this.props.translate('additionalDetailsStep.helpTextIdologyQuestions')}
+
+ {this.props.translate('additionalDetailsStep.helpLink')}
+
+
+ this.form = el}>
+
+ {question.prompt}
+ this.chooseAnswer(questionIndex, answer)}
+ />
+
+
+ {
+ this.form.scrollTo({y: 0, animated: true});
+ }}
+ message={this.state.errorMessage}
+ isLoading={this.state.isLoading}
+ buttonText={this.props.translate('common.saveAndContinue')}
+ />
+
+
+ );
+ }
+}
+
+IdologyQuestions.propTypes = propTypes;
+IdologyQuestions.defaultProps = defaultProps;
+export default withLocalize(IdologyQuestions);
diff --git a/tests/unit/WalletTest.js b/tests/unit/WalletTest.js
new file mode 100644
index 000000000000..464a48e09db5
--- /dev/null
+++ b/tests/unit/WalletTest.js
@@ -0,0 +1,30 @@
+const wallet = require('../../src/libs/actions/Wallet');
+
+describe('Wallet', () => {
+ it('Test buildIdologyError returning the right error copy based on Idology errors passed', () => {
+ // Test address error
+ expect(wallet.buildIdologyError(['resultcode.address.does.not.match'])).toBe('We could not verify your personal address. Please fix this information before continuing.');
+
+ // Test date of birth error
+ expect(wallet.buildIdologyError(['resultcode.age.below.minimum'])).toBe('We could not verify your date of birth. Please fix this information before continuing.');
+
+ // Test SSN error
+ expect(wallet.buildIdologyError(['resultcode.ssn.does.not.match'])).toBe('We could not verify your SSN. Please fix this information before continuing.');
+
+ // Test lastName error
+ expect(wallet.buildIdologyError(['resultcode.last.name.does.not.match'])).toBe('We could not verify your legal last name. Please fix this information before continuing.');
+
+ // Test multiple errors
+ expect(wallet.buildIdologyError([
+ 'resultcode.zip.does.not.match',
+ 'resultcode.yob.within.one.year',
+ 'resultcode.ssn.not.valid',
+ ])).toBe('We could not verify your personal address, your date of birth and your SSN. Please fix this information before continuing.');
+
+ // Test unknown errors
+ expect(wallet.buildIdologyError([
+ 'whatever1',
+ 'whatever2',
+ ])).toBe('');
+ });
+});