diff --git a/src/CONST.ts b/src/CONST.ts index 9e7c1f007335..46526cdbdafd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -165,6 +165,18 @@ const CONST = { DOMAIN: '@expensify.sms', }, BANK_ACCOUNT: { + PERSONAL_INFO_STEP: { + INPUT_KEY: { + FIRST_NAME: 'firstName', + LAST_NAME: 'lastName', + DOB: 'dob', + SSN_LAST_4: 'ssnLast4', + STREET: 'requestorAddressStreet', + CITY: 'requestorAddressCity', + STATE: 'requestorAddressState', + ZIP_CODE: 'requestorAddressZipCode', + }, + }, PLAID: { ALLOWED_THROTTLED_COUNT: 2, ERROR: { diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js index 55abcc1fc923..5f20e25104f3 100644 --- a/src/components/Form/FormWrapper.js +++ b/src/components/Form/FormWrapper.js @@ -56,6 +56,10 @@ const propTypes = { /** Container styles */ style: stylePropTypes, + /** Submit button container styles */ + // eslint-disable-next-line react/forbid-prop-types + submitButtonStyles: PropTypes.arrayOf(PropTypes.object), + /** Custom content to display in the footer after submit button */ footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), @@ -74,10 +78,25 @@ const defaultProps = { scrollContextEnabled: false, footerContent: null, style: [], + submitButtonStyles: [], }; function FormWrapper(props) { - const {onSubmit, children, formState, errors, inputRefs, submitButtonText, footerContent, isSubmitButtonVisible, style, enabledWhenOffline, isSubmitActionDangerous, formID} = props; + const { + onSubmit, + children, + formState, + errors, + inputRefs, + submitButtonText, + footerContent, + isSubmitButtonVisible, + style, + enabledWhenOffline, + isSubmitActionDangerous, + formID, + submitButtonStyles, + } = props; const formRef = useRef(null); const formContentRef = useRef(null); const errorMessage = useMemo(() => { @@ -129,7 +148,7 @@ function FormWrapper(props) { focusInput.focus(); } }} - containerStyles={[styles.mh0, styles.mt5, styles.flex1]} + containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...submitButtonStyles]} enabledWhenOffline={enabledWhenOffline} isSubmitActionDangerous={isSubmitActionDangerous} disablePressOnEnter @@ -150,6 +169,7 @@ function FormWrapper(props) { isSubmitActionDangerous, isSubmitButtonVisible, onSubmit, + submitButtonStyles, style, submitButtonText, ], diff --git a/src/components/InteractiveStepSubHeader.js b/src/components/InteractiveStepSubHeader.js index 75fdce8ad3b5..0405d2bd9ede 100644 --- a/src/components/InteractiveStepSubHeader.js +++ b/src/components/InteractiveStepSubHeader.js @@ -1,16 +1,15 @@ -import React, {forwardRef, useState, useImperativeHandle} from 'react'; -import PropTypes from 'prop-types'; import map from 'lodash/map'; +import PropTypes from 'prop-types'; +import React, {forwardRef, useImperativeHandle, useState} from 'react'; import {View} from 'react-native'; - -import CONST from '../CONST'; -import variables from '../styles/variables'; -import styles from '../styles/styles'; -import colors from '../styles/colors'; +import colors from '@styles/colors'; +import styles from '@styles/styles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import Text from './Text'; -import Icon from './Icon'; const propTypes = { /** List of the Route Name to navigate when the step is selected */ diff --git a/src/components/NewDatePicker/index.js b/src/components/NewDatePicker/index.js index f03b4e2cb796..66e4a337df2f 100644 --- a/src/components/NewDatePicker/index.js +++ b/src/components/NewDatePicker/index.js @@ -9,6 +9,7 @@ import TextInput from '@components/TextInput'; import {propTypes as baseTextInputPropTypes, defaultProps as defaultBaseTextInputPropTypes} from '@components/TextInput/baseTextInputPropTypes'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import styles from '@styles/styles'; +import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import CalendarPicker from './CalendarPicker'; @@ -33,6 +34,12 @@ const propTypes = { /** A maximum date of calendar to select */ maxDate: PropTypes.objectOf(Date), + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft: PropTypes.bool, + + /** ID of the wrapping form */ + formID: PropTypes.string, + ...withLocalizePropTypes, ...baseTextInputPropTypes, }; @@ -42,17 +49,42 @@ const datePickerDefaultProps = { minDate: setYear(new Date(), CONST.CALENDAR_PICKER.MIN_YEAR), maxDate: setYear(new Date(), CONST.CALENDAR_PICKER.MAX_YEAR), value: undefined, + shouldSaveDraft: false, + formID: '', }; -function NewDatePicker({containerStyles, defaultValue, disabled, errorText, inputID, isSmallScreenWidth, label, maxDate, minDate, onInputChange, onTouched, placeholder, translate, value}) { +function NewDatePicker({ + containerStyles, + defaultValue, + disabled, + errorText, + inputID, + isSmallScreenWidth, + label, + maxDate, + minDate, + onInputChange, + onTouched, + placeholder, + translate, + value, + shouldSaveDraft, + formID, +}) { const [selectedDate, setSelectedDate] = useState(value || defaultValue || undefined); useEffect(() => { + // Value is provided to input via props and onChange never fires. We have to save draft manually. + if (shouldSaveDraft && formID !== '') { + FormActions.setDraftValues(formID, {[inputID]: selectedDate}); + } + if (selectedDate === value || _.isUndefined(value)) { return; } + setSelectedDate(value); - }, [selectedDate, value]); + }, [formID, inputID, selectedDate, shouldSaveDraft, value]); useEffect(() => { if (_.isFunction(onTouched)) { diff --git a/src/hooks/useSubStep.js b/src/hooks/useSubStep.js index 86071e278913..bfd88f5d5c57 100644 --- a/src/hooks/useSubStep.js +++ b/src/hooks/useSubStep.js @@ -1,5 +1,5 @@ -import {useState, useRef, useCallback} from 'react'; import PropTypes from 'prop-types'; +import {useCallback, useRef, useState} from 'react'; const propTypes = { /** an array of substep components */ diff --git a/src/languages/en.ts b/src/languages/en.ts index c186a1fffedf..8772e4b3eaac 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -167,6 +167,7 @@ export default { noPO: 'PO boxes and mail drop addresses are not allowed', city: 'City', state: 'State', + streetAddress: 'Street address', stateOrProvince: 'State / Province', country: 'Country', zip: 'Zip code', @@ -266,6 +267,7 @@ export default { tbd: 'TBD', selectCurrency: 'Select a currency', card: 'Card', + whyDoWeAskForThis: 'Why do we ask for this?', }, location: { useCurrent: 'Use current location', @@ -1347,13 +1349,27 @@ export default { }, requestorStep: { headerTitle: 'Personal information', - subtitle: 'Please provide your personal information.', learnMore: 'Learn more', isMyDataSafe: 'Is my data safe?', onFidoConditions: 'By continuing with the request to add this bank account, you confirm that you have read, understand and accept ', isControllingOfficer: 'I am authorized to use my company bank account for business spend', isControllingOfficerError: 'You must be a controlling officer with authorization to operate the business bank account.', }, + personalInfoStep: { + personalInfo: 'Personal info', + enterYourLegalFirstAndLast: 'Enter your legal first and last name.', + legalFirstName: 'Legal first name', + legalLastName: 'Legal last name', + legalName: 'Legal name', + enterYourDateOfBirth: 'Enter your date of birth.', + enterTheLast4: 'Enter the last 4 of your SSN.', + dontWorry: "Don't worry, we don't do any personal credit checks!", + last4SSN: 'Last 4 Social Security Number', + enterYourAddress: 'Enter your address.', + address: 'Address', + letsDoubleCheck: "Let's double check everything looks right", + byAddingThisBankAccount: 'By adding this bank account, you confirm that you have read, understand and accept', + }, validationStep: { headerTitle: 'Validate Bank Account', buttonText: 'Finish setup', diff --git a/src/languages/es.ts b/src/languages/es.ts index a0a30bcf4141..06b559170d0e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -157,6 +157,7 @@ export default { noPO: 'No se aceptan apartados ni direcciones postales', city: 'Ciudad', state: 'Estado', + streetAddress: 'Dirección', stateOrProvince: 'Estado / Provincia', country: 'País', zip: 'Código postal', @@ -256,6 +257,7 @@ export default { tbd: 'Por determinar', selectCurrency: 'Selecciona una moneda', card: 'Tarjeta', + whyDoWeAskForThis: '¿Por qué pedimos esto?', }, location: { useCurrent: 'Usar ubicación actual', @@ -1366,13 +1368,27 @@ export default { }, requestorStep: { headerTitle: 'Información personal', - subtitle: 'Dé más información sobre tí.', learnMore: 'Más información', isMyDataSafe: '¿Están seguros mis datos?', onFidoConditions: 'Al continuar con la solicitud de añadir esta cuenta bancaria, confirma que ha leído, entiende y acepta ', isControllingOfficer: 'Estoy autorizado a utilizar la cuenta bancaria de mi compañía para gastos de empresa', isControllingOfficerError: 'Debe ser un oficial controlador con autorización para operar la cuenta bancaria de la compañía', }, + personalInfoStep: { + personalInfo: 'Información Personal', + enterYourLegalFirstAndLast: 'Ingrese su Nombre y Apellido', + legalFirstName: 'Nombre', + legalLastName: 'Apellido', + legalName: 'Nombre legal', + enterYourDateOfBirth: 'Ingrese su fecha de Cumple años', + enterTheLast4: 'Ingrese los últimos 4 dígitos de su NSS', + dontWorry: 'No se preocupe, no hacemos ninguna verificación de créditos', + last4SSN: 'Últimos 4 dígitos de su Número de Seguro Social', + enterYourAddress: 'Ingrese su dirección', + address: 'Dirección', + letsDoubleCheck: 'Revisemos que todo esté bien', + byAddingThisBankAccount: 'Agregando esta cuenta bancaria, confirmas que as leído, entendido y aceptado', + }, validationStep: { headerTitle: 'Validar cuenta bancaria', buttonText: 'Finalizar configuración', diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index b8675fd9cc0e..0f63b6a3eb92 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -104,7 +104,6 @@ function AddressForm(props) { defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} - hint={props.translate('common.noPO')} renamedInputKeys={props.inputKeys} maxInputLength={CONST.FORM_CHARACTER_LIMIT} isLimitedToUSA @@ -145,7 +144,6 @@ function AddressForm(props) { onChangeText={(value) => props.onFieldChange({zipCode: value})} errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} containerStyles={[styles.mt2]} /> diff --git a/src/pages/ReimbursementAccount/PersonalInfo/HelpLinks.js b/src/pages/ReimbursementAccount/PersonalInfo/HelpLinks.js new file mode 100644 index 000000000000..aee1b59f8e15 --- /dev/null +++ b/src/pages/ReimbursementAccount/PersonalInfo/HelpLinks.js @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import styles from '@styles/styles'; +import CONST from '@src/CONST'; + +const propTypes = { + /** Style for wrapping View */ + // eslint-disable-next-line react/forbid-prop-types + containerStyles: PropTypes.arrayOf(PropTypes.object), + + /** Translate function */ + translate: PropTypes.func.isRequired, +}; + +const defaultProps = { + containerStyles: [], +}; + +function HelpLinks({containerStyles, translate}) { + return ( + + + + + {translate('requestorStep.learnMore')} + + {' | '} + + {translate('requestorStep.isMyDataSafe')} + + + + ); +} + +HelpLinks.displayName = 'HelpLinks'; +HelpLinks.propTypes = propTypes; +HelpLinks.defaultProps = defaultProps; + +export default HelpLinks; diff --git a/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.js b/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.js new file mode 100644 index 000000000000..218e7438d06e --- /dev/null +++ b/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.js @@ -0,0 +1,109 @@ +import _ from 'lodash'; +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useSubStep from '@hooks/useSubStep'; +import Navigation from '@libs/Navigation/Navigation'; +import reimbursementAccountDraftPropTypes from '@pages/ReimbursementAccount/ReimbursementAccountDraftPropTypes'; +import {reimbursementAccountPropTypes} from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; +import getInitialSubstepForPersonalInfo from '@pages/ReimbursementAccount/utils/getInitialSubstepForPersonalInfo'; +import getPersonalInfoValues from '@pages/ReimbursementAccount/utils/getPersonalInfoValues'; +import styles from '@styles/styles'; +import * as BankAccounts from '@userActions/BankAccounts'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import Address from './substeps/Address'; +import Confirmation from './substeps/Confirmation'; +import DateOfBirth from './substeps/DateOfBirth'; +import FullName from './substeps/FullName'; +import SocialSecurityNumber from './substeps/SocialSecurityNumber'; + +const propTypes = { + /** Reimbursement account from ONYX */ + reimbursementAccount: reimbursementAccountPropTypes, + + /** The draft values of the bank account being setup */ + reimbursementAccountDraft: reimbursementAccountDraftPropTypes, +}; + +const defaultProps = { + reimbursementAccount: ReimbursementAccountProps.reimbursementAccountDefaultProps, + reimbursementAccountDraft: {}, +}; + +const STEPS_HEADER_HEIGHT = 40; +// TODO Will most likely come from different place +const STEP_NAMES = ['1', '2', '3', '4', '5']; + +const bodyContent = [FullName, DateOfBirth, SocialSecurityNumber, Address, Confirmation]; + +function PersonalInfo({reimbursementAccount, reimbursementAccountDraft}) { + const {translate} = useLocalize(); + + const values = useMemo(() => getPersonalInfoValues(reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); + + const submit = useCallback(() => { + const payload = { + bankAccountID: _.get(reimbursementAccount, 'achData.bankAccountID', 0), + ...values, + }; + + BankAccounts.updatePersonalInformationForBankAccount(payload); + }, [reimbursementAccount, values]); + const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(values), [values]); + + const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo} = useSubStep({bodyContent, startFrom, onFinished: submit}); + + const handleBackButtonPress = () => { + if (screenIndex === 0) { + Navigation.goBack(ROUTES.HOME); + } else { + prevScreen(); + } + }; + + return ( + + + + {}} + // TODO Will be replaced with proper values + startStep={1} + stepNames={STEP_NAMES} + /> + + + + ); +} + +PersonalInfo.propTypes = propTypes; +PersonalInfo.defaultProps = defaultProps; +PersonalInfo.displayName = 'PersonalInfo'; + +export default withOnyx({ + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, + reimbursementAccountDraft: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + }, +})(PersonalInfo); diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.js b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.js new file mode 100644 index 000000000000..b50ee2f22e3a --- /dev/null +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.js @@ -0,0 +1,103 @@ +import lodashGet from 'lodash/get'; +import React from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import Form from '@components/Form'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import AddressForm from '@pages/ReimbursementAccount/AddressForm'; +import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; +import {reimbursementAccountPropTypes} from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; +import subStepPropTypes from '@pages/ReimbursementAccount/subStepPropTypes'; +import styles from '@styles/styles'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const propTypes = { + /** Reimbursement account from ONYX */ + reimbursementAccount: reimbursementAccountPropTypes, + + ...subStepPropTypes, +}; + +const defaultProps = { + reimbursementAccount: ReimbursementAccountProps.reimbursementAccountDefaultProps, +}; + +const INPUT_KEYS = { + street: CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.STREET, + city: CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.CITY, + state: CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.STATE, + zipCode: CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.ZIP_CODE, +}; + +const REQUIRED_FIELDS = [ + CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.STREET, + CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.CITY, + CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.STATE, + CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.ZIP_CODE, +]; + +const validate = (values) => { + const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS); + + if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) { + errors.requestorAddressStreet = 'bankAccount.error.addressStreet'; + } + + if (values.requestorAddressZipCode && !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) { + errors.requestorAddressZipCode = 'bankAccount.error.zipCode'; + } + + return errors; +}; + +function Address({reimbursementAccount, onNext, isEditing}) { + const {translate} = useLocalize(); + + const defaultValues = { + street: lodashGet(reimbursementAccount, ['achData', CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.STREET], ''), + city: lodashGet(reimbursementAccount, ['achData', CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.CITY], ''), + state: lodashGet(reimbursementAccount, ['achData', CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.STATE], ''), + zipCode: lodashGet(reimbursementAccount, ['achData', CONST.BANK_ACCOUNT.PERSONAL_INFO_STEP.INPUT_KEY.ZIP_CODE], ''), + }; + + return ( +
+ + {translate('personalInfoStep.enterYourAddress')} + {translate('common.noPO')} + + + +
+ ); +} + +Address.propTypes = propTypes; +Address.defaultProps = defaultProps; +Address.displayName = 'Address'; + +export default withOnyx({ + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, +})(Address); diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.js b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.js new file mode 100644 index 000000000000..03d0ca5f5b25 --- /dev/null +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.js @@ -0,0 +1,139 @@ +import React, {useMemo} from 'react'; +import {ScrollView, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import DotIndicatorMessage from '@components/DotIndicatorMessage'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import reimbursementAccountDraftPropTypes from '@pages/ReimbursementAccount/ReimbursementAccountDraftPropTypes'; +import {reimbursementAccountPropTypes} from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; +import subStepPropTypes from '@pages/ReimbursementAccount/subStepPropTypes'; +import getPersonalInfoValues from '@pages/ReimbursementAccount/utils/getPersonalInfoValues'; +import styles from '@styles/styles'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const propTypes = { + /** Reimbursement account from ONYX */ + reimbursementAccount: reimbursementAccountPropTypes, + + /** The draft values of the bank account being setup */ + reimbursementAccountDraft: reimbursementAccountDraftPropTypes, + + ...subStepPropTypes, +}; + +const defaultProps = { + reimbursementAccount: ReimbursementAccountProps.reimbursementAccountDefaultProps, + reimbursementAccountDraft: {}, +}; + +function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext, onMove}) { + const {translate} = useLocalize(); + + const values = useMemo(() => getPersonalInfoValues(reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); + const error = ErrorUtils.getLatestErrorMessage(reimbursementAccount); + + return ( + + + {translate('personalInfoStep.letsDoubleCheck')} + { + onMove(0); + }} + /> + { + onMove(1); + }} + /> + { + onMove(2); + }} + /> + { + onMove(3); + }} + /> + + + {`${translate('personalInfoStep.byAddingThisBankAccount')} `} + + {translate('onfidoStep.facialScan')} + + {', '} + + {translate('common.privacy')} + + {` ${translate('common.and')} `} + + {translate('common.termsOfService')} + + + + {error.length > 0 && ( + + )} +