Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f21cc76
initial rewrite of RequestorStep
kacper-mikolajczak Sep 14, 2023
062cfac
add forwardRef compatibility
kacper-mikolajczak Sep 14, 2023
f4e9c7d
extraction of constants and memoization
kacper-mikolajczak Sep 14, 2023
c2c3f35
extract styles
kacper-mikolajczak Sep 14, 2023
6dfd524
use INPUT_KEYS for form default values object
kacper-mikolajczak Sep 15, 2023
8e1d212
use lodashGet defaultValue arg
kacper-mikolajczak Sep 15, 2023
16169e8
remove redundant template literals
kacper-mikolajczak Sep 15, 2023
0110dfe
add displayName
kacper-mikolajczak Sep 15, 2023
a20cb71
swap withLocalize for useLocalize
kacper-mikolajczak Sep 15, 2023
43e915e
add braces to early return
kacper-mikolajczak Sep 15, 2023
7f5655f
Revert "extract styles"
kacper-mikolajczak Sep 15, 2023
22ae3d3
rename LabelComponent to renderLabelComponent
kacper-mikolajczak Sep 15, 2023
e32147b
propTypes omit improvement
kacper-mikolajczak Sep 15, 2023
0e0c07c
extract urls
kacper-mikolajczak Sep 15, 2023
c75ad40
remove renderLabelComponent memoization
kacper-mikolajczak Sep 15, 2023
4ede3a6
Merge branch 'main'
kacper-mikolajczak Sep 19, 2023
6e5fa8a
fix lodash issues
kacper-mikolajczak Sep 19, 2023
375c85f
remove unused ref
kacper-mikolajczak Sep 19, 2023
9de749d
fix PropTypes
kacper-mikolajczak Sep 22, 2023
67116fc
Merge branch 'main' into refactor/16247/requestor-step-class-to-function
kacper-mikolajczak Sep 22, 2023
f127451
add PropTypes to inner forward ref component
kacper-mikolajczak Sep 22, 2023
08ba6ac
fix displayName issues and remove inner component
kacper-mikolajczak Sep 22, 2023
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
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@ const CONST = {
INTERNAL_DEV_EXPENSIFY_URL: 'https://www.expensify.com.dev',
STAGING_EXPENSIFY_URL: 'https://staging.expensify.com',
EXPENSIFY_URL: 'https://www.expensify.com',
BANK_ACCOUNT_PERSONAL_DOCUMENTATION_INFO_URL:
'https://community.expensify.com/discussion/6983/faq-why-do-i-need-to-provide-personal-documentation-when-setting-up-updating-my-bank-account',
PERSONAL_DATA_PROTECTION_INFO_URL: 'https://community.expensify.com/discussion/5677/deep-dive-security-how-expensify-protects-your-information',
ONFIDO_FACIAL_SCAN_POLICY_URL: 'https://onfido.com/facial-scan-policy-and-release/',
ONFIDO_PRIVACY_POLICY_URL: 'https://onfido.com/privacy/',
ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/',

// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'http://localhost:',
Expand Down
319 changes: 162 additions & 157 deletions src/pages/ReimbursementAccount/RequestorStep.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React from 'react';
import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
import _ from 'lodash';
import styles from '../../styles/styles';
import withLocalize from '../../components/withLocalize';
import HeaderWithBackButton from '../../components/HeaderWithBackButton';
import CONST from '../../CONST';
import TextLink from '../../components/TextLink';
Expand All @@ -16,182 +15,188 @@ import ONYXKEYS from '../../ONYXKEYS';
import RequestorOnfidoStep from './RequestorOnfidoStep';
import Form from '../../components/Form';
import ScreenWrapper from '../../components/ScreenWrapper';
import StepPropTypes from './StepPropTypes';
import useLocalize from '../../hooks/useLocalize';
import {reimbursementAccountPropTypes} from './reimbursementAccountPropTypes';
import ReimbursementAccountDraftPropTypes from './ReimbursementAccountDraftPropTypes';

const propTypes = {
...StepPropTypes,
onBackButtonPress: PropTypes.func.isRequired,
getDefaultStateForField: PropTypes.func.isRequired,
reimbursementAccount: reimbursementAccountPropTypes.isRequired,
reimbursementAccountDraft: ReimbursementAccountDraftPropTypes.isRequired,

/** If we should show Onfido flow */
shouldShowOnfido: PropTypes.bool.isRequired,
};

class RequestorStep extends React.Component {
constructor(props) {
super(props);

this.validate = this.validate.bind(this);
this.submit = this.submit.bind(this);
}

/**
* @param {Object} values
* @returns {Object}
*/
validate(values) {
const requiredFields = ['firstName', 'lastName', 'dob', 'ssnLast4', 'requestorAddressStreet', 'requestorAddressCity', 'requestorAddressState', 'requestorAddressZipCode'];
const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields);

if (values.dob) {
if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) {
errors.dob = 'bankAccount.error.dob';
} else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) {
errors.dob = 'bankAccount.error.age';
}
}

if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) {
errors.ssnLast4 = 'bankAccount.error.ssnLast4';
}
const REQUIRED_FIELDS = ['firstName', 'lastName', 'dob', 'ssnLast4', 'requestorAddressStreet', 'requestorAddressCity', 'requestorAddressState', 'requestorAddressZipCode'];
const INPUT_KEYS = {
firstName: 'firstName',
lastName: 'lastName',
dob: 'dob',
ssnLast4: 'ssnLast4',
street: 'requestorAddressStreet',
city: 'requestorAddressCity',
state: 'requestorAddressState',
zipCode: 'requestorAddressZipCode',
};
const STEP_COUNTER = {step: 3, total: 5};

if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) {
errors.requestorAddressStreet = 'bankAccount.error.addressStreet';
}
const validate = (values) => {
const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS);

if (values.requestorAddressZipCode && !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) {
errors.requestorAddressZipCode = 'bankAccount.error.zipCode';
if (values.dob) {
if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) {
errors.dob = 'bankAccount.error.dob';
} else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) {
errors.dob = 'bankAccount.error.age';
}
}

if (!ValidationUtils.isRequiredFulfilled(values.isControllingOfficer)) {
errors.isControllingOfficer = 'requestorStep.isControllingOfficerError';
}
if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) {
errors.ssnLast4 = 'bankAccount.error.ssnLast4';
}

return errors;
if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) {
errors.requestorAddressStreet = 'bankAccount.error.addressStreet';
}

submit(values) {
const payload = {
bankAccountID: lodashGet(this.props.reimbursementAccount, 'achData.bankAccountID') || 0,
...values,
};
if (values.requestorAddressZipCode && !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) {
errors.requestorAddressZipCode = 'bankAccount.error.zipCode';
}

BankAccounts.updatePersonalInformationForBankAccount(payload);
if (!ValidationUtils.isRequiredFulfilled(values.isControllingOfficer)) {
errors.isControllingOfficer = 'requestorStep.isControllingOfficerError';
}

render() {
if (this.props.shouldShowOnfido) {
return (
<RequestorOnfidoStep
reimbursementAccount={this.props.reimbursementAccount}
reimbursementAccountDraft={this.props.reimbursementAccountDraft}
onBackButtonPress={this.props.onBackButtonPress}
getDefaultStateForField={this.props.getDefaultStateForField}
/>
);
}
return errors;
};

function RequestorStep({reimbursementAccount, shouldShowOnfido, reimbursementAccountDraft, onBackButtonPress, getDefaultStateForField}) {
const {translate} = useLocalize();

const defaultValues = useMemo(
() => ({
firstName: getDefaultStateForField(INPUT_KEYS.firstName),
lastName: getDefaultStateForField(INPUT_KEYS.lastName),
street: getDefaultStateForField(INPUT_KEYS.street),
city: getDefaultStateForField(INPUT_KEYS.city),
state: getDefaultStateForField(INPUT_KEYS.state),
zipCode: getDefaultStateForField(INPUT_KEYS.zipCode),
dob: getDefaultStateForField(INPUT_KEYS.dob),
ssnLast4: getDefaultStateForField(INPUT_KEYS.ssnLast4),
}),
[getDefaultStateForField],
);

const submit = useCallback(
(values) => {
const payload = {
bankAccountID: _.get(reimbursementAccount, 'achData.bankAccountID', 0),
...values,
};

BankAccounts.updatePersonalInformationForBankAccount(payload);
},
[reimbursementAccount],
);

const renderLabelComponent = () => (
<View style={[styles.flex1, styles.pr1]}>
<Text>{translate('requestorStep.isControllingOfficer')}</Text>
</View>
);

if (shouldShowOnfido) {
return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={RequestorStep.displayName}
>
<HeaderWithBackButton
title={this.props.translate('requestorStep.headerTitle')}
stepCounter={{step: 3, total: 5}}
shouldShowGetAssistanceButton
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BANK_ACCOUNT}
onBackButtonPress={this.props.onBackButtonPress}
/>
<Form
formID={ONYXKEYS.REIMBURSEMENT_ACCOUNT}
submitButtonText={this.props.translate('common.saveAndContinue')}
validate={this.validate}
scrollContextEnabled
onSubmit={this.submit}
style={[styles.mh5, styles.flexGrow1]}
>
<Text>{this.props.translate('requestorStep.subtitle')}</Text>
<View style={[styles.mb5, styles.mt1, styles.dFlex, styles.flexRow]}>
<TextLink
style={[styles.textMicro]}
// eslint-disable-next-line max-len
href="https://community.expensify.com/discussion/6983/faq-why-do-i-need-to-provide-personal-documentation-when-setting-up-updating-my-bank-account"
>
{`${this.props.translate('requestorStep.learnMore')}`}
</TextLink>
<Text style={[styles.textMicroSupporting]}>{' | '}</Text>
<TextLink
style={[styles.textMicro, styles.textLink]}
// eslint-disable-next-line max-len
href="https://community.expensify.com/discussion/5677/deep-dive-security-how-expensify-protects-your-information"
>
{`${this.props.translate('requestorStep.isMyDataSafe')}`}
</TextLink>
</View>
<IdentityForm
translate={this.props.translate}
defaultValues={{
firstName: this.props.getDefaultStateForField('firstName'),
lastName: this.props.getDefaultStateForField('lastName'),
street: this.props.getDefaultStateForField('requestorAddressStreet'),
city: this.props.getDefaultStateForField('requestorAddressCity'),
state: this.props.getDefaultStateForField('requestorAddressState'),
zipCode: this.props.getDefaultStateForField('requestorAddressZipCode'),
dob: this.props.getDefaultStateForField('dob'),
ssnLast4: this.props.getDefaultStateForField('ssnLast4'),
}}
inputKeys={{
firstName: 'firstName',
lastName: 'lastName',
dob: 'dob',
ssnLast4: 'ssnLast4',
street: 'requestorAddressStreet',
city: 'requestorAddressCity',
state: 'requestorAddressState',
zipCode: 'requestorAddressZipCode',
}}
shouldSaveDraft
/>
<CheckboxWithLabel
accessibilityLabel={this.props.translate('requestorStep.isControllingOfficer')}
inputID="isControllingOfficer"
defaultValue={this.props.getDefaultStateForField('isControllingOfficer', false)}
LabelComponent={() => (
<View style={[styles.flex1, styles.pr1]}>
<Text>{this.props.translate('requestorStep.isControllingOfficer')}</Text>
</View>
)}
style={[styles.mt4]}
shouldSaveDraft
/>
<Text style={[styles.mt3, styles.textMicroSupporting]}>
{this.props.translate('requestorStep.onFidoConditions')}
<TextLink
href="https://onfido.com/facial-scan-policy-and-release/"
style={[styles.textMicro]}
>
{this.props.translate('onfidoStep.facialScan')}
</TextLink>
{', '}
<TextLink
href="https://onfido.com/privacy/"
style={[styles.textMicro]}
>
{this.props.translate('common.privacy')}
</TextLink>
{` ${this.props.translate('common.and')} `}
<TextLink
href="https://onfido.com/terms-of-service/"
style={[styles.textMicro]}
>
{this.props.translate('common.termsOfService')}
</TextLink>
</Text>
</Form>
</ScreenWrapper>
<RequestorOnfidoStep
reimbursementAccount={reimbursementAccount}
reimbursementAccountDraft={reimbursementAccountDraft}
onBackButtonPress={onBackButtonPress}
getDefaultStateForField={getDefaultStateForField}
/>
);
}

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={RequestorStep.displayName}
>
<HeaderWithBackButton
title={translate('requestorStep.headerTitle')}
stepCounter={STEP_COUNTER}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BANK_ACCOUNT}
onBackButtonPress={onBackButtonPress}
shouldShowGetAssistanceButton
/>
<Form
formID={ONYXKEYS.REIMBURSEMENT_ACCOUNT}
submitButtonText={translate('common.saveAndContinue')}
validate={validate}
onSubmit={submit}
style={[styles.mh5, styles.flexGrow1]}
scrollContextEnabled
>
<Text>{translate('requestorStep.subtitle')}</Text>
<View style={[styles.mb5, styles.mt1, styles.dFlex, styles.flexRow]}>
<TextLink
style={[styles.textMicro]}
href={CONST.BANK_ACCOUNT_PERSONAL_DOCUMENTATION_INFO_URL}
>
{translate('requestorStep.learnMore')}
</TextLink>
<Text style={[styles.textMicroSupporting]}>{' | '}</Text>
<TextLink
style={[styles.textMicro, styles.textLink]}
href={CONST.PERSONAL_DATA_PROTECTION_INFO_URL}
>
{translate('requestorStep.isMyDataSafe')}
</TextLink>
</View>
<IdentityForm
translate={translate}
defaultValues={defaultValues}
inputKeys={INPUT_KEYS}
shouldSaveDraft
/>
<CheckboxWithLabel
accessibilityLabel={translate('requestorStep.isControllingOfficer')}
inputID="isControllingOfficer"
defaultValue={getDefaultStateForField('isControllingOfficer', false)}
LabelComponent={renderLabelComponent}
style={[styles.mt4]}
shouldSaveDraft
/>
<Text style={[styles.mt3, styles.textMicroSupporting]}>
{translate('requestorStep.onFidoConditions')}
<TextLink
href={CONST.ONFIDO_FACIAL_SCAN_POLICY_URL}
style={[styles.textMicro]}
>
{translate('onfidoStep.facialScan')}
</TextLink>
{', '}
<TextLink
href={CONST.ONFIDO_PRIVACY_POLICY_URL}
style={[styles.textMicro]}
>
{translate('common.privacy')}
</TextLink>
{` ${translate('common.and')} `}
<TextLink
href={CONST.ONFIDO_TERMS_OF_SERVICE_URL}
style={[styles.textMicro]}
>
{translate('common.termsOfService')}
</TextLink>
</Text>
</Form>
</ScreenWrapper>
);
}

RequestorStep.propTypes = propTypes;
RequestorStep.displayName = 'RequestorStep';

export default withLocalize(RequestorStep);
export default React.forwardRef(RequestorStep);