Skip to content

Commit

Permalink
P2P KYC - Handle Idology questions
Browse files Browse the repository at this point in the history
  • Loading branch information
nkuoch committed Feb 18, 2022
1 parent 4b04f73 commit 4aba17e
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 90 deletions.
4 changes: 4 additions & 0 deletions src/CONST/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,12 @@ const CONST = {
UNEXPECTED: 'Unexpected error',
MISSING_FIELD: 'Missing required additional details fields',
UNABLE_TO_VERIFY: 'Unable to verify identity',

// KBA stands for Knowledge Based Answers (requiring us to show Idology questions)
KBA_NEEDED: 'KBA needed',
},
STEP: {
// In the order they appear in the Wallet flow
ONFIDO: 'OnfidoStep',
ADDITIONAL_DETAILS: 'AdditionalDetailsStep',
TERMS: 'TermsStep',
Expand Down
170 changes: 170 additions & 0 deletions src/components/IdologyQuestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import _ from 'underscore';
import React from 'react';
import PropTypes from 'prop-types';
import {
View,
} from 'react-native';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import styles from '../styles/styles';
import * as BankAccounts from '../libs/actions/BankAccounts';
import CONST from '../CONST';
import Text from './Text';
import TextLink from './TextLink';
import FormScrollView from './FormScrollView';
import FormAlertWithSubmitButton from './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),
})),
};

const defaultProps = {
questions: [],
};

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: '',
};
}

/**
* 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 Onfido 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 IQ 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});
return {answers};
}

// 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] || {};
return (
<View style={[styles.flex1]}>
<View style={[styles.ph5]}>
<Text style={styles.mb3}>{this.props.translate('additionalDetailsStep.helpTextIdologyQuestions')}</Text>
<TextLink
style={styles.mb3}
href="https://use.expensify.com/usa-patriot-act"
>
{this.props.translate('additionalDetailsStep.helpLink')}
</TextLink>
</View>
<FormScrollView ref={el => this.form = el}>
<View style={[styles.mh5, styles.mb5]} key={question.type}>
<View className="chooseText marginBottom30">
<strong>{question.prompt}</strong>
</View>
<View className="control-group">
<ul className="chooseList">
{_.map(question.answer, (answer, answerIndex) => {
if (this.state.hideSkip && answer === SKIP_QUESTION_TEXT) {
return;
}

return (
<li key={answer}>
<input
type="radio"
name={`answer_${questionIndex}`}
id={`answer_${questionIndex}_${answerIndex}`}
onChange={() => this.chooseAnswer(questionIndex, answer)}
/>
<label htmlFor={`answer_${questionIndex}_${answerIndex}`} className="marginLeft">
{answer}
</label>
</li>
);
})}
</ul>
</View>
</View>

<FormAlertWithSubmitButton
isAlertVisible={Boolean(this.state.errorMessage)}
onSubmit={this.submitAnswers}
onFixTheErrorsLinkPressed={() => {
this.form.scrollTo({y: 0, animated: true});
}}
message={this.state.errorMessage}
buttonText={this.props.translate('common.saveAndContinue')}
/>
</FormScrollView>
</View>
);
}
}

IdologyQuestions.propTypes = propTypes;
IdologyQuestions.defaultProps = defaultProps;
export default withLocalize(IdologyQuestions);
3 changes: 2 additions & 1 deletion src/components/TextInput/BaseTextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ class BaseTextInput extends Component {

componentDidUpdate() {
// Activate or deactivate the label when value is changed programmatically from outside
if (this.value === this.props.value) {
// Only update when value prop is provided
if (this.props.value === undefined || this.value === this.props.value) {
return;
}

Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,10 +559,12 @@ 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: 'Please select an answer',
},
termsStep: {
headerTitle: 'Terms and fees',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,10 +559,12 @@ 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',
},
termsStep: {
headerTitle: 'Condiciones y tarifas',
Expand Down
1 change: 1 addition & 0 deletions src/libs/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,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}
*/
Expand Down
14 changes: 14 additions & 0 deletions src/libs/actions/Wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ function setAdditionalDetailsLoading(loading) {
Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {loading});
}

/**
* @param {Array} questions
*/
function setAdditionalDetailsQuestions(questions) {
Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {questions});
}

/**
* @param {Object} errorFields
*/
Expand Down Expand Up @@ -71,6 +78,7 @@ function setAdditionalDetailsErrorMessage(additionalErrorMessage) {
*/
function activateWallet(currentStep, parameters) {
let personalDetails;
let idologyAnswers;
let onfidoData;
let hasAcceptedTerms;

Expand All @@ -86,6 +94,7 @@ function activateWallet(currentStep, parameters) {
setAdditionalDetailsErrors(null);
setAdditionalDetailsErrorMessage('');
personalDetails = JSON.stringify(parameters.personalDetails);
idologyAnswers = JSON.stringify({answers: parameters.idologyAnswers});
} else if (currentStep === CONST.WALLET.STEP.TERMS) {
hasAcceptedTerms = parameters.hasAcceptedTerms;
Onyx.merge(ONYXKEYS.WALLET_TERMS, {loading: true});
Expand All @@ -94,6 +103,7 @@ function activateWallet(currentStep, parameters) {
API.Wallet_Activate({
currentStep,
personalDetails,
idologyAnswers,
onfidoData,
hasAcceptedTerms,
})
Expand All @@ -105,6 +115,10 @@ function activateWallet(currentStep, parameters) {
}

if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) {
if (response.title === CONST.WALLET.ERROR.KBA_NEEDED) {
setAdditionalDetailsQuestions(response.data.questions);
}

if (response.title === CONST.WALLET.ERROR.MISSING_FIELD) {
// 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
Expand Down
Loading

0 comments on commit 4aba17e

Please sign in to comment.