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

Handle Wallet Onfido flow #8093

Merged
merged 4 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ const CONST = {
FULL_SSN_NOT_FOUND: 'Full SSN not found',
MISSING_FIELD: 'Missing required additional details fields',
WRONG_ANSWERS: 'Wrong answers',
ONFIDO_FIXABLE_ERROR: 'Onfido returned a fixable error',

// KBA stands for Knowledge Based Answers (requiring us to show Idology questions)
KBA_NEEDED: 'KBA needed',
Expand All @@ -442,8 +443,8 @@ const CONST = {
},
STEP: {
// In the order they appear in the Wallet flow
ONFIDO: 'OnfidoStep',
ADDITIONAL_DETAILS: 'AdditionalDetailsStep',
ONFIDO: 'OnfidoStep',
TERMS: 'TermsStep',
ACTIVATE: 'ActivateStep',
},
Expand Down
6 changes: 6 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,12 @@ export default {
genericError: 'There was an error while processing this step. Please try again.',
cameraPermissionsNotGranted: 'Camera permissions not granted',
cameraRequestMessage: 'You have not granted us camera access. We need access to complete verification.',
originalDocumentNeeded: 'Please upload an original image of your ID rather than a screenshot or scanned image.',
documentNeedsBetterQuality: 'Your ID appears to be damaged or has missing security features. Please upload an original image of an undamaged ID that is entirely visible.',
imageNeedsBetterQuality: 'There\'s an issue with the image quality of your ID. Please upload a new image where your entire ID can be seen clearly.',
selfieIssue: 'There\'s an issue with your selfie/video. Please upload a new selfie/video in real time.',
selfieNotMatching: 'Your selfie/video doesn\'t match your ID. Please upload a new selfie/video where your face can be clearly seen.',
selfieNotLive: 'Your selfie/video doesn\'t appear to be a live photo/video. Please upload a live selfie/video.',
},
additionalDetailsStep: {
headerTitle: 'Additional details',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,12 @@ export default {
genericError: 'Hubo un error al procesar este paso. Inténtalo de nuevo.',
cameraPermissionsNotGranted: 'No has habilitado los permisos para acceder a la cámara',
cameraRequestMessage: 'No has habilitado los permisos para acceder a la cámara. Necesitamos acceso para completar la verificaciôn.',
originalDocumentNeeded: 'Por favor, sube una imagen original de tu identificación en lugar de una captura de pantalla o imagen escaneada.',
documentNeedsBetterQuality: 'Parece que tu identificación esta dañado o le faltan características de seguridad. Por favor, sube una imagen de tu documento sin daños y que se vea completamente.',
imageNeedsBetterQuality: 'Hay un problema con la calidad de la imagen de tu identificación. Por favor, sube una nueva imagen donde el identificación se vea con claridad.',
selfieIssue: 'Hay un problema con tu selfie/video. Por favor, sube un nuevo selfie/video grabado en el momento',
selfieNotMatching: 'Tu selfie/video no concuerda con tu identificación. Por favor, sube un nuevo selfie/video donde se vea tu cara con claridad.',
selfieNotLive: 'Tu selfie/video no parece ser un selfie/video en vivo. Por favor, sube un selfie/video a tiempo real.',
},
additionalDetailsStep: {
headerTitle: 'Detalles adicionales',
Expand Down
8 changes: 7 additions & 1 deletion src/libs/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -898,14 +898,20 @@ function CreateIOUSplit(parameters) {
}

/**
* @param {String} firstName
* @param {String} lastName
* @param {String} dob
* @returns {Promise}
*/
function Wallet_GetOnfidoSDKToken() {
function Wallet_GetOnfidoSDKToken(firstName, lastName, dob) {
return Network.post('Wallet_GetOnfidoSDKToken', {
// We need to pass this so we can request a token with the correct referrer
// This value comes from a cross-platform module which returns true for native
// platforms and false for non-native platforms.
isViaExpensifyCashNative,
firstName,
lastName,
dob,
}, CONST.NETWORK.METHOD.POST, true);
}

Expand Down
38 changes: 38 additions & 0 deletions src/libs/actions/PersonalDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,43 @@ function fetchPersonalDetails() {
});
}

/**
* Gets the first and last name from the user's personal details.
* If the login is the same as the displayName, then they don't exist,
* so we return empty strings instead.
* @param {Object} personalDetail
* @param {String} personalDetail.login
* @param {String} personalDetail.displayName
* @param {String} personalDetail.firstName
* @param {String} personalDetail.lastName
*
* @returns {Object}
*/
function extractFirstAndLastNameFromAvailableDetails({
login,
displayName,
firstName,
lastName,
}) {
if (firstName || lastName) {
return {firstName: firstName || '', lastName: lastName || ''};
}
if (Str.removeSMSDomain(login) === displayName) {
return {firstName: '', lastName: ''};
}

const firstSpaceIndex = displayName.indexOf(' ');
const lastSpaceIndex = displayName.lastIndexOf(' ');
if (firstSpaceIndex === -1) {
return {firstName: displayName, lastName: ''};
}

return {
firstName: displayName.substring(0, firstSpaceIndex).trim(),
lastName: displayName.substring(lastSpaceIndex).trim(),
};
}

/**
* Get personal details from report participants.
*
Expand Down Expand Up @@ -350,4 +387,5 @@ export {
fetchLocalCurrency,
getCurrencyList,
getMaxCharacterError,
extractFirstAndLastNameFromAvailableDetails,
};
35 changes: 26 additions & 9 deletions src/libs/actions/Wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import * as Localize from '../Localize';
* - The sdkToken is used to initialize the Onfido SDK client
* - The applicantID is combined with the data returned from the Onfido SDK as we need both to create an
* identity check. Note: This happens in Web-Secure when we call Activate_Wallet during the OnfidoStep.
* @param {String} firstName
* @param {String} lastName
* @param {String} dob
*/
function fetchOnfidoToken() {
function fetchOnfidoToken(firstName, lastName, dob) {
// Use Onyx.set() since we are resetting the Onfido flow completely.
Onyx.set(ONYXKEYS.WALLET_ONFIDO, {loading: true});
API.Wallet_GetOnfidoSDKToken()
API.Wallet_GetOnfidoSDKToken(firstName, lastName, dob)
.then((response) => {
const apiResult = lodashGet(response, ['requestorIdentityOnfido', 'apiResult'], {});
Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {
Expand Down Expand Up @@ -71,8 +74,8 @@ function setAdditionalDetailsShouldAskForFullSSN(shouldAskForFullSSN) {
/**
* @param {Boolean} shouldShowFailedKYC
*/
function setAdditionalDetailsShouldShowFailedKYC(shouldShowFailedKYC) {
Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {shouldShowFailedKYC});
function setWalletShouldShowFailedKYC(shouldShowFailedKYC) {
Onyx.merge(ONYXKEYS.USER_WALLET, {shouldShowFailedKYC});
}

/**
Expand Down Expand Up @@ -175,6 +178,7 @@ function activateWallet(currentStep, parameters) {
throw new Error('Invalid currentStep passed to activateWallet()');
}

setWalletShouldShowFailedKYC(false);
if (currentStep === CONST.WALLET.STEP.ONFIDO) {
onfidoData = parameters.onfidoData;
Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {error: '', loading: true});
Expand All @@ -183,7 +187,6 @@ function activateWallet(currentStep, parameters) {
setAdditionalDetailsLoading(true);
setAdditionalDetailsErrors(null);
setAdditionalDetailsErrorMessage('');
setAdditionalDetailsShouldShowFailedKYC(false);
personalDetails = JSON.stringify(parameters.personalDetails);
}
if (parameters.idologyAnswers) {
Expand Down Expand Up @@ -212,7 +215,12 @@ function activateWallet(currentStep, parameters) {

if (response.jsonCode !== 200) {
if (currentStep === CONST.WALLET.STEP.ONFIDO) {
Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {error: response.message, loading: false});
Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {loading: false});
if (response.title === CONST.WALLET.ERROR.ONFIDO_FIXABLE_ERROR) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the UUID or any type of error code for this instead? As it stands, the error message sounds more likely to change in Web-Secure and break this flow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the pattern we've been using everywhere else in this method. I agree with you, although I'm not a fan of using the uuid either.
I think the proper way would be either via another type of exception php (extending from ExpError), or via a key errorType in data? (Either way, let's do it in a follow up PR, so I can do it for all the other places too).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to try to start a wider conversation about this and propose a holistic fix. There's a lot of inconsistency in the new app in general WRT error handling - in many places we look at jsonCode in others the response.message etc.

Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {fixableErrors: lodashGet(response, 'data.fixableErrors', [])});
return;
}
setWalletShouldShowFailedKYC(true);
return;
}

Expand Down Expand Up @@ -257,7 +265,7 @@ function activateWallet(currentStep, parameters) {
'resultcode.pa.dob.does.not.match',
];
if (_.some(hardFailures, hardFailure => _.contains(idologyErrors, hardFailure))) {
setAdditionalDetailsShouldShowFailedKYC(true);
setWalletShouldShowFailedKYC(true);
return;
}

Expand All @@ -270,7 +278,7 @@ function activateWallet(currentStep, parameters) {

if (lodashGet(response, 'data.requestorIdentityID.apiResult.results.key') === 'result.no.match'
|| response.title === CONST.WALLET.ERROR.WRONG_ANSWERS) {
setAdditionalDetailsShouldShowFailedKYC(true);
setWalletShouldShowFailedKYC(true);
return;
}
if (Str.endsWith(response.type, 'AutoVerifyFailure')) {
Expand Down Expand Up @@ -318,7 +326,8 @@ function fetchUserWallet() {
return;
}

Onyx.merge(ONYXKEYS.USER_WALLET, response.userWallet);
// When refreshing the wallet, we should not show the failed KYC page anymore, as we should allow them to retry.
Onyx.merge(ONYXKEYS.USER_WALLET, {...response.userWallet, shouldShowFailedKYC: false});
});
}

Expand All @@ -329,6 +338,13 @@ function updateAdditionalDetailsDraft(keyValuePair) {
Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS_DRAFT, keyValuePair);
}

/**
* @param {String} currentStep
*/
function updateCurrentStep(currentStep) {
Onyx.merge(ONYXKEYS.USER_WALLET, {currentStep});
}

export {
fetchOnfidoToken,
activateWallet,
Expand All @@ -338,4 +354,5 @@ export {
setAdditionalDetailsErrorMessage,
setAdditionalDetailsQuestions,
buildIdologyError,
updateCurrentStep,
};
46 changes: 12 additions & 34 deletions src/pages/EnablePayments/AdditionalDetailsStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ 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';
import walletAdditionalDetailsDraftPropTypes from './walletAdditionalDetailsDraftPropTypes';
import personalDetailsPropType from '../personalDetailsPropType';
import * as PersonalDetails from '../../libs/actions/PersonalDetails';

const propTypes = {
...withLocalizePropTypes,
Expand All @@ -52,25 +54,15 @@ const propTypes = {
/** 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,
}),
walletAdditionalDetailsDraft: walletAdditionalDetailsDraftPropTypes,

/** The personal details of the person who is logged in */
myPersonalDetails: personalDetailsPropType.isRequired,
};

const defaultProps = {
Expand All @@ -80,7 +72,6 @@ const defaultProps = {
additionalErrorMessage: '',
questions: [],
idNumber: '',
shouldShowFailedKYC: false,
shouldAskForFullSSN: false,
},
walletAdditionalDetailsDraft: {
Expand Down Expand Up @@ -212,20 +203,6 @@ class AdditionalDetailsStep extends React.Component {
}

render() {
if (this.props.walletAdditionalDetails.shouldShowFailedKYC) {
return (
<ScreenWrapper>
<KeyboardAvoidingView style={[styles.flex1]} behavior="height">
<HeaderWithCloseButton
title={this.props.translate('additionalDetailsStep.headerTitle')}
onCloseButtonPress={() => Navigation.dismissModal()}
/>
<FailedKYC />
</KeyboardAvoidingView>
</ScreenWrapper>
);
}

if (!_.isEmpty(this.props.walletAdditionalDetails.questions)) {
return (
<ScreenWrapper>
Expand All @@ -248,6 +225,7 @@ class AdditionalDetailsStep extends React.Component {
const isErrorVisible = _.size(this.getErrors()) > 0
|| lodashGet(this.props, 'walletAdditionalDetails.additionalErrorMessage', '').length > 0;
const shouldAskForFullSSN = this.props.walletAdditionalDetails.shouldAskForFullSSN;
const {firstName, lastName} = PersonalDetails.extractFirstAndLastNameFromAvailableDetails(this.props.myPersonalDetails);

return (
<ScreenWrapper>
Expand All @@ -273,14 +251,14 @@ class AdditionalDetailsStep extends React.Component {
containerStyles={[styles.mt4]}
label={this.props.translate(this.fieldNameTranslationKeys.legalFirstName)}
onChangeText={val => this.clearErrorAndSetValue('legalFirstName', val)}
value={this.props.walletAdditionalDetailsDraft.legalFirstName || ''}
value={this.props.walletAdditionalDetailsDraft.legalFirstName || firstName}
errorText={this.getErrorText('legalFirstName')}
/>
<TextInput
containerStyles={[styles.mt4]}
label={this.props.translate(this.fieldNameTranslationKeys.legalLastName)}
onChangeText={val => this.clearErrorAndSetValue('legalLastName', val)}
value={this.props.walletAdditionalDetailsDraft.legalLastName || ''}
value={this.props.walletAdditionalDetailsDraft.legalLastName || lastName}
errorText={this.getErrorText('legalLastName')}
/>
<AddressSearch
Expand Down Expand Up @@ -385,8 +363,8 @@ export default compose(
key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS,
initWithStoredValues: false,
},
walletAdditionalDetailsDraft: {
key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS_DRAFT,
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
myPersonalDetails: {
key: ONYXKEYS.MY_PERSONAL_DETAILS,
},
}),
)(AdditionalDetailsStep);
Loading