diff --git a/src/CONST.js b/src/CONST.js index 5c1fd52f8798..d85dd5a36e15 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -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', @@ -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', }, diff --git a/src/languages/en.js b/src/languages/en.js index a17924c1f7dc..f70ec35135cd 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -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', diff --git a/src/languages/es.js b/src/languages/es.js index a247568bb3e5..a401e6811437 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -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', diff --git a/src/libs/API.js b/src/libs/API.js index 1fbfe935e5bf..0b32d2c245b7 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -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); } diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 0fe6cd893dcf..fdad13b97569 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -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. * @@ -350,4 +387,5 @@ export { fetchLocalCurrency, getCurrencyList, getMaxCharacterError, + extractFirstAndLastNameFromAvailableDetails, }; diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 05fb2e8dc7b8..9ff5c67fce1b 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -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, { @@ -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}); } /** @@ -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}); @@ -183,7 +187,6 @@ function activateWallet(currentStep, parameters) { setAdditionalDetailsLoading(true); setAdditionalDetailsErrors(null); setAdditionalDetailsErrorMessage(''); - setAdditionalDetailsShouldShowFailedKYC(false); personalDetails = JSON.stringify(parameters.personalDetails); } if (parameters.idologyAnswers) { @@ -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) { + Onyx.merge(ONYXKEYS.WALLET_ONFIDO, {fixableErrors: lodashGet(response, 'data.fixableErrors', [])}); + return; + } + setWalletShouldShowFailedKYC(true); return; } @@ -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; } @@ -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')) { @@ -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}); }); } @@ -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, @@ -338,4 +354,5 @@ export { setAdditionalDetailsErrorMessage, setAdditionalDetailsQuestions, buildIdologyError, + updateCurrentStep, }; diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index 1fc82c82354e..58c96e46f58d 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -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, @@ -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 = { @@ -80,7 +72,6 @@ const defaultProps = { additionalErrorMessage: '', questions: [], idNumber: '', - shouldShowFailedKYC: false, shouldAskForFullSSN: false, }, walletAdditionalDetailsDraft: { @@ -212,20 +203,6 @@ class AdditionalDetailsStep extends React.Component { } render() { - if (this.props.walletAdditionalDetails.shouldShowFailedKYC) { - return ( - - - Navigation.dismissModal()} - /> - - - - ); - } - if (!_.isEmpty(this.props.walletAdditionalDetails.questions)) { return ( @@ -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 ( @@ -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')} /> this.clearErrorAndSetValue('legalLastName', val)} - value={this.props.walletAdditionalDetailsDraft.legalLastName || ''} + value={this.props.walletAdditionalDetailsDraft.legalLastName || lastName} errorText={this.getErrorText('legalLastName')} /> ; } - const currentStep = this.props.userWallet.currentStep || CONST.WALLET.STEP.ONFIDO; + if (this.props.userWallet.shouldShowFailedKYC) { + return ( + + + Navigation.dismissModal()} + /> + + + + ); + } + + const currentStep = this.props.userWallet.currentStep || CONST.WALLET.STEP.ADDITIONAL_DETAILS; + return ( - {currentStep === CONST.WALLET.STEP.ONFIDO && } - {currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS && } + {currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS && } + {currentStep === CONST.WALLET.STEP.ONFIDO && } {currentStep === CONST.WALLET.STEP.TERMS && } {currentStep === CONST.WALLET.STEP.ACTIVATE && } @@ -47,12 +69,18 @@ class EnablePaymentsPage extends React.Component { EnablePaymentsPage.propTypes = propTypes; EnablePaymentsPage.defaultProps = defaultProps; -export default withOnyx({ - userWallet: { - key: ONYXKEYS.USER_WALLET, +export default compose( + withLocalize, + withOnyx({ + userWallet: { + key: ONYXKEYS.USER_WALLET, - // We want to refresh the wallet each time the user attempts to activate the wallet so we won't use the - // stored values here. - initWithStoredValues: false, - }, -})(EnablePaymentsPage); + // We want to refresh the wallet each time the user attempts to activate the wallet so we won't use the + // stored values here. + initWithStoredValues: false, + }, + walletAdditionalDetailsDraft: { + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS_DRAFT, + }, + }), +)(EnablePaymentsPage); diff --git a/src/pages/EnablePayments/OnfidoPrivacy.js b/src/pages/EnablePayments/OnfidoPrivacy.js new file mode 100644 index 000000000000..c65b82ae1fa1 --- /dev/null +++ b/src/pages/EnablePayments/OnfidoPrivacy.js @@ -0,0 +1,134 @@ +import React from 'react'; +import {View} from 'react-native'; +import lodashGet from 'lodash/get'; +import _ from 'underscore'; +import {withOnyx} from 'react-native-onyx'; +import FullscreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; +import ONYXKEYS from '../../ONYXKEYS'; +import * as BankAccounts from '../../libs/actions/BankAccounts'; +import styles from '../../styles/styles'; +import TextLink from '../../components/TextLink'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import compose from '../../libs/compose'; +import Text from '../../components/Text'; +import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; +import FormScrollView from '../../components/FormScrollView'; +import walletAdditionalDetailsDraftPropTypes from './walletAdditionalDetailsDraftPropTypes'; +import walletOnfidoDataPropTypes from './walletOnfidoDataPropTypes'; +import * as Localize from '../../libs/Localize'; + +const propTypes = { + /** Stores various information used to build the UI and call any APIs */ + walletOnfidoData: walletOnfidoDataPropTypes, + + /** Stores the personal details typed by the user */ + walletAdditionalDetailsDraft: walletAdditionalDetailsDraftPropTypes.isRequired, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + walletOnfidoData: { + applicantID: '', + sdkToken: '', + loading: false, + error: '', + hasAcceptedPrivacyPolicy: false, + }, +}; + +class OnfidoPrivacy extends React.Component { + constructor(props) { + super(props); + + this.fetchOnfidoToken = this.fetchOnfidoToken.bind(this); + } + + fetchOnfidoToken() { + BankAccounts.fetchOnfidoToken( + this.props.walletAdditionalDetailsDraft.legalFirstName, + this.props.walletAdditionalDetailsDraft.legalLastName, + this.props.walletAdditionalDetailsDraft.dob, + ); + } + + render() { + let onfidoError = lodashGet(this.props, 'walletOnfidoData.error') || ''; + if (!onfidoError) { + const onfidoFixableErrors = lodashGet(this.props, 'userWallet.onfidoFixableErrors', []); + if (!_.isEmpty(onfidoFixableErrors)) { + const supportedErrorKeys = ['originalDocumentNeeded', 'documentNeedsBetterQuality', 'imageNeedsBetterQuality', 'selfieIssue', 'selfieNotMatching', 'selfieNotLive']; + const translatedFixableErrors = _.filter(_.map(onfidoFixableErrors, (errorKey) => { + if (_.contains(supportedErrorKeys, errorKey)) { + return Localize.translateLocal(`onfidoStep.${errorKey}`); + } + return null; + })); + if (!_.isEmpty(translatedFixableErrors)) { + onfidoError = translatedFixableErrors.join(' '); + } + } + } + + return ( + + {!this.props.walletOnfidoData.hasAcceptedPrivacyPolicy ? ( + this.form = el}> + + + {this.props.translate('onfidoStep.acceptTerms')} + + {this.props.translate('onfidoStep.facialScan')} + + {', '} + + {this.props.translate('common.privacyPolicy')} + + {` ${this.props.translate('common.and')} `} + + {this.props.translate('common.termsOfService')} + + . + + + { + this.form.scrollTo({y: 0, animated: true}); + }} + message={onfidoError} + isLoading={this.props.walletOnfidoData.loading} + buttonText={onfidoError ? this.props.translate('onfidoStep.tryAgain') : this.props.translate('common.continue')} + /> + + ) : null} + {this.props.walletOnfidoData.hasAcceptedPrivacyPolicy && this.props.walletOnfidoData.loading ? : null} + + ); + } +} + +OnfidoPrivacy.propTypes = propTypes; +OnfidoPrivacy.defaultProps = defaultProps; + +export default compose( + withLocalize, + withOnyx({ + walletOnfidoData: { + key: ONYXKEYS.WALLET_ONFIDO, + + // Let's get a new onfido token each time the user hits this flow (as it should only be once) + initWithStoredValues: false, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + }), +)(OnfidoPrivacy); diff --git a/src/pages/EnablePayments/OnfidoStep.js b/src/pages/EnablePayments/OnfidoStep.js index e3ef5fe3beb7..16ea1f897bcc 100644 --- a/src/pages/EnablePayments/OnfidoStep.js +++ b/src/pages/EnablePayments/OnfidoStep.js @@ -1,41 +1,26 @@ import React from 'react'; -import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; import Onfido from '../../components/Onfido'; -import FullscreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; import ONYXKEYS from '../../ONYXKEYS'; import * as BankAccounts from '../../libs/actions/BankAccounts'; import Navigation from '../../libs/Navigation/Navigation'; import CONST from '../../CONST'; -import Button from '../../components/Button'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; -import styles from '../../styles/styles'; -import TextLink from '../../components/TextLink'; +import * as Wallet from '../../libs/actions/Wallet'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; -import Text from '../../components/Text'; import Growl from '../../libs/Growl'; +import OnfidoPrivacy from './OnfidoPrivacy'; +import walletAdditionalDetailsDraftPropTypes from './walletAdditionalDetailsDraftPropTypes'; +import walletOnfidoDataPropTypes from './walletOnfidoDataPropTypes'; const propTypes = { /** Stores various information used to build the UI and call any APIs */ - walletOnfidoData: PropTypes.shape({ + walletOnfidoData: walletOnfidoDataPropTypes, - /** Unique identifier returned from fetchOnfidoToken then re-sent to ActivateWallet with Onfido response data */ - applicantID: PropTypes.string, - - /** Token used to initialize the Onfido SDK token */ - sdkToken: PropTypes.string, - - /** Loading state to provide feedback when we are waiting for a request to finish */ - loading: PropTypes.bool, - - /** Error message to inform the user of any problem that might occur */ - error: PropTypes.string, - - /** Whether the user has accepted the privacy policy of Onfido or not */ - hasAcceptedPrivacyPolicy: PropTypes.bool, - }), + /** Stores the personal details typed by the user */ + walletAdditionalDetailsDraft: walletAdditionalDetailsDraftPropTypes.isRequired, ...withLocalizePropTypes, }; @@ -48,6 +33,17 @@ const defaultProps = { }; class OnfidoStep extends React.Component { + componentDidMount() { + // Once in Onfido step, if we somehow don't have the personal info, go back to previous step, as we need them for Onfido{ + const firstName = lodashGet(this.props, 'walletAdditionalDetailsDraft.legalFirstName'); + const lastName = lodashGet(this.props, 'walletAdditionalDetailsDraft.legalLastName'); + const dob = lodashGet(this.props, 'walletAdditionalDetailsDraft.dob'); + + if (!firstName || !lastName || !dob) { + Wallet.updateCurrentStep(CONST.WALLET.STEP.ADDITIONAL_DETAILS); + } + } + /** * @returns {boolean|*} */ @@ -65,7 +61,7 @@ class OnfidoStep extends React.Component { title={this.props.translate('onfidoStep.verifyIdentity')} onCloseButtonPress={() => Navigation.dismissModal()} shouldShowBackButton - onBackButtonPress={() => Navigation.goBack()} + onBackButtonPress={() => Wallet.updateCurrentStep(CONST.WALLET.STEP.ADDITIONAL_DETAILS)} /> { this.canShowOnfido() ? ( @@ -87,60 +83,7 @@ class OnfidoStep extends React.Component { }} /> ) : ( - - {!this.props.walletOnfidoData.hasAcceptedPrivacyPolicy && ( - <> - - - {this.props.translate('onfidoStep.acceptTerms')} - - {this.props.translate('onfidoStep.facialScan')} - - {', '} - - {this.props.translate('common.privacyPolicy')} - - {` ${this.props.translate('common.and')} `} - - {this.props.translate('common.termsOfService')} - - . - - -