Skip to content

Commit

Permalink
Merge pull request #34498 from barttom/vit-tieredBankAccountFlow
Browse files Browse the repository at this point in the history
[Wave 6] Bank Account Flow Refactor
  • Loading branch information
mountiny authored Feb 13, 2024
2 parents fb69cc1 + 8e4a414 commit b49c384
Show file tree
Hide file tree
Showing 95 changed files with 5,480 additions and 1,588 deletions.
108 changes: 107 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,71 @@ const CONST = {
DOMAIN: '@expensify.sms',
},
BANK_ACCOUNT: {
BANK_INFO_STEP: {
INPUT_KEY: {
ROUTING_NUMBER: 'routingNumber',
ACCOUNT_NUMBER: 'accountNumber',
PLAID_MASK: 'plaidMask',
IS_SAVINGS: 'isSavings',
BANK_NAME: 'bankName',
PLAID_ACCOUNT_ID: 'plaidAccountID',
PLAID_ACCESS_TOKEN: 'plaidAccessToken',
},
},
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',
},
},
BUSINESS_INFO_STEP: {
INPUT_KEY: {
COMPANY_NAME: 'companyName',
COMPANY_TAX_ID: 'companyTaxID',
COMPANY_WEBSITE: 'website',
COMPANY_PHONE: 'companyPhone',
STREET: 'addressStreet',
CITY: 'addressCity',
STATE: 'addressState',
ZIP_CODE: 'addressZipCode',
INCORPORATION_TYPE: 'incorporationType',
INCORPORATION_DATE: 'incorporationDate',
INCORPORATION_STATE: 'incorporationState',
HAS_NO_CONNECTION_TO_CANNABIS: 'hasNoConnectionToCannabis',
},
},
BENEFICIAL_OWNER_INFO_STEP: {
SUBSTEP: {
IS_USER_UBO: 1,
IS_ANYONE_ELSE_UBO: 2,
UBO_DETAILS_FORM: 3,
ARE_THERE_MORE_UBOS: 4,
UBOS_LIST: 5,
},
INPUT_KEY: {
OWNS_MORE_THAN_25_PERCENT: 'ownsMoreThan25Percent',
HAS_OTHER_BENEFICIAL_OWNERS: 'hasOtherBeneficialOwners',
BENEFICIAL_OWNERS: 'beneficialOwners',
},
BENEFICIAL_OWNER_DATA: {
BENEFICIAL_OWNER_KEYS: 'beneficialOwnerKeys',
PREFIX: 'beneficialOwner',
FIRST_NAME: 'firstName',
LAST_NAME: 'lastName',
DOB: 'dob',
SSN_LAST_4: 'ssnLast4',
STREET: 'street',
CITY: 'city',
STATE: 'state',
ZIP_CODE: 'zipCode',
},
},
PLAID: {
ALLOWED_THROTTLED_COUNT: 2,
ERROR: {
Expand All @@ -205,6 +270,13 @@ const CONST = {
EXIT: 'EXIT',
},
},
COMPLETE_VERIFICATION: {
INPUT_KEY: {
IS_AUTHORIZED_TO_USE_BANK_ACCOUNT: 'isAuthorizedToUseBankAccount',
CERTIFY_TRUE_INFORMATION: 'certifyTrueInformation',
ACCEPT_TERMS_AND_CONDITIONS: 'acceptTermsAndConditions',
},
},
ERROR: {
MISSING_ROUTING_NUMBER: '402 Missing routingNumber',
MAX_ROUTING_NUMBER: '402 Maximum Size Exceeded routingNumber',
Expand All @@ -214,14 +286,18 @@ const CONST = {
STEP: {
// In the order they appear in the VBA flow
BANK_ACCOUNT: 'BankAccountStep',
COMPANY: 'CompanyStep',
REQUESTOR: 'RequestorStep',
COMPANY: 'CompanyStep',
BENEFICIAL_OWNERS: 'BeneficialOwnersStep',
ACH_CONTRACT: 'ACHContractStep',
VALIDATION: 'ValidationStep',
ENABLE: 'EnableStep',
},
STEP_NAMES: ['1', '2', '3', '4', '5'],
STEPS_HEADER_HEIGHT: 40,
SUBSTEP: {
MANUAL: 'manual',
PLAID: 'plaid',
},
VERIFICATIONS: {
ERROR_MESSAGE: 'verifications.errorMessage',
Expand Down Expand Up @@ -493,6 +569,8 @@ const CONST = {
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/',
LIST_OF_RESTRICTED_BUSINESSES: 'https://community.expensify.com/discussion/6191/list-of-restricted-businesses',

// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:',
OLDDOT_URLS: {
Expand Down Expand Up @@ -3194,6 +3272,34 @@ const CONST = {
},

REPORT_FIELD_TITLE_FIELD_ID: 'text_title',

REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX: {
BANK_ACCOUNT: {
ACCOUNT_NUMBERS: 0,
},
PERSONAL_INFO: {
LEGAL_NAME: 0,
DATE_OF_BIRTH: 1,
SSN: 2,
ADDRESS: 3,
},
BUSINESS_INFO: {
BUSINESS_NAME: 0,
TAX_ID_NUMBER: 1,
COMPANY_WEBSITE: 2,
PHONE_NUMBER: 3,
COMPANY_ADDRESS: 4,
COMPANY_TYPE: 5,
INCORPORATION_DATE: 6,
INCORPORATION_STATE: 7,
},
UBO: {
LEGAL_NAME: 0,
DATE_OF_BIRTH: 1,
SSN: 2,
ADDRESS: 3,
},
},
} as const;

type Country = keyof typeof CONST.ALL_COUNTRIES;
Expand Down
6 changes: 4 additions & 2 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const ONYXKEYS = {

/** Token needed to initialize Onfido */
ONFIDO_TOKEN: 'onfidoToken',
ONFIDO_APPLICANT_ID: 'onfidoApplicantID',

/** Indicates which locale should be used */
NVP_PREFERRED_LOCALE: 'preferredLocale',
Expand Down Expand Up @@ -426,6 +427,7 @@ type OnyxValues = {
[ONYXKEYS.IS_PLAID_DISABLED]: boolean;
[ONYXKEYS.PLAID_LINK_TOKEN]: string;
[ONYXKEYS.ONFIDO_TOKEN]: string;
[ONYXKEYS.ONFIDO_APPLICANT_ID]: string;
[ONYXKEYS.NVP_PREFERRED_LOCALE]: OnyxTypes.Locale;
[ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet;
[ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido;
Expand Down Expand Up @@ -563,8 +565,8 @@ type OnyxValues = {
[ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: OnyxTypes.ReportFieldEditForm;
[ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM_DRAFT]: OnyxTypes.Form;
// @ts-expect-error Different values are defined under the same key: ReimbursementAccount and ReimbursementAccountForm
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: OnyxTypes.Form;
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form;
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: OnyxTypes.ReimbursementAccountForm;
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.ReimbursementAccountForm;
[ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount;
[ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_DRAFT]: OnyxTypes.PersonalBankAccount;
};
Expand Down
74 changes: 71 additions & 3 deletions src/components/AddPlaidBankAccount.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef} from 'react';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
Expand All @@ -16,10 +16,12 @@ import * as BankAccounts from '@userActions/BankAccounts';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlockingView';
import FormHelpMessage from './FormHelpMessage';
import Icon from './Icon';
import getBankIcon from './Icon/BankIcons';
import Picker from './Picker';
import PlaidLink from './PlaidLink';
import RadioButtons from './RadioButtons';
import Text from './Text';

const propTypes = {
Expand Down Expand Up @@ -55,6 +57,15 @@ const propTypes = {

/** Are we adding a withdrawal account? */
allowDebit: PropTypes.bool,

/** Is displayed in new VBBA */
isDisplayedInNewVBBA: PropTypes.bool,

/** Text to display on error message */
errorText: PropTypes.string,

/** Function called whenever radio button value changes */
onInputChange: PropTypes.func,
};

const defaultProps = {
Expand All @@ -68,6 +79,9 @@ const defaultProps = {
allowDebit: false,
bankAccountID: 0,
isPlaidDisabled: false,
isDisplayedInNewVBBA: false,
errorText: '',
onInputChange: () => {},
};

function AddPlaidBankAccount({
Expand All @@ -82,11 +96,23 @@ function AddPlaidBankAccount({
bankAccountID,
allowDebit,
isPlaidDisabled,
isDisplayedInNewVBBA,
errorText,
onInputChange,
}) {
const theme = useTheme();
const styles = useThemeStyles();
const plaidBankAccounts = lodashGet(plaidData, 'bankAccounts', []);
const defaultSelectedPlaidAccount = _.find(plaidBankAccounts, (account) => account.plaidAccountID === selectedPlaidAccountID);
const defaultSelectedPlaidAccountID = lodashGet(defaultSelectedPlaidAccount, 'plaidAccountID', '');
const defaultSelectedPlaidAccountMask = lodashGet(
_.find(plaidBankAccounts, (account) => account.plaidAccountID === selectedPlaidAccountID),
'mask',
'',
);
const subscribedKeyboardShortcuts = useRef([]);
const previousNetworkState = useRef();
const [selectedPlaidAccountMask, setSelectedPlaidAccountMask] = useState(defaultSelectedPlaidAccountMask);

const {translate} = useLocalize();
const {isOffline} = useNetwork();
Expand Down Expand Up @@ -162,17 +188,28 @@ function AddPlaidBankAccount({
previousNetworkState.current = isOffline;
}, [allowDebit, bankAccountID, isAuthenticatedWithPlaid, isOffline]);

const plaidBankAccounts = lodashGet(plaidData, 'bankAccounts') || [];
const token = getPlaidLinkToken();
const options = _.map(plaidBankAccounts, (account) => ({
value: account.plaidAccountID,
label: `${account.addressName} ${account.mask}`,
label: account.addressName,
}));
const {icon, iconSize, iconStyles} = getBankIcon({styles});
const plaidErrors = lodashGet(plaidData, 'errors');
const plaidDataErrorMessage = !_.isEmpty(plaidErrors) ? _.chain(plaidErrors).values().first().value() : '';
const bankName = lodashGet(plaidData, 'bankName');

/**
* @param {String} plaidAccountID
*
* When user selects one of plaid accounts we need to set the mask in order to display it on UI
*/
const handleSelectingPlaidAccount = (plaidAccountID) => {
const mask = _.find(plaidBankAccounts, (account) => account.plaidAccountID === plaidAccountID).mask;
setSelectedPlaidAccountMask(mask);
onSelect(plaidAccountID);
onInputChange(plaidAccountID);
};

if (isPlaidDisabled) {
return (
<View>
Expand Down Expand Up @@ -239,6 +276,37 @@ function AddPlaidBankAccount({
return <FullPageOfflineBlockingView>{renderPlaidLink()}</FullPageOfflineBlockingView>;
}

if (isDisplayedInNewVBBA) {
return (
<FullPageOfflineBlockingView>
<Text style={[styles.mb3, styles.textHeadline]}>{translate('bankAccount.chooseAnAccount')}</Text>
{!_.isEmpty(text) && <Text style={[styles.mb6, styles.textSupporting]}>{text}</Text>}
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mb6]}>
<Icon
src={icon}
height={iconSize}
width={iconSize}
additionalStyles={iconStyles}
/>
<View>
<Text style={[styles.ml3, styles.textStrong]}>{bankName}</Text>
{selectedPlaidAccountMask.length > 0 && (
<Text style={[styles.ml3, styles.textLabelSupporting]}>{`${translate('bankAccount.accountEnding')} ${selectedPlaidAccountMask}`}</Text>
)}
</View>
</View>
<Text style={[styles.textLabelSupporting]}>{`${translate('bankAccount.chooseAnAccountBelow')}:`}</Text>
<RadioButtons
items={options}
defaultCheckedValue={defaultSelectedPlaidAccountID}
onPress={handleSelectingPlaidAccount}
radioButtonStyle={[styles.mb6]}
/>
<FormHelpMessage message={errorText} />
</FullPageOfflineBlockingView>
);
}

// Plaid bank accounts view
return (
<FullPageOfflineBlockingView>
Expand Down
43 changes: 41 additions & 2 deletions src/components/DatePicker/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {setYear} from 'date-fns';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, {forwardRef, useState} from 'react';
import React, {forwardRef, useEffect, useState} from 'react';
import {View} from 'react-native';
import * as Expensicons from '@components/Icon/Expensicons';
import refPropTypes from '@components/refPropTypes';
import TextInput from '@components/TextInput';
import {propTypes as baseTextInputPropTypes, defaultProps as defaultBaseTextInputPropTypes} from '@components/TextInput/BaseTextInput/baseTextInputPropTypes';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as FormActions from '@userActions/FormActions';
import CONST from '@src/CONST';
import CalendarPicker from './CalendarPicker';

Expand Down Expand Up @@ -42,6 +43,12 @@ const propTypes = {
/** A function that is passed by FormWrapper */
onTouched: PropTypes.func.isRequired,

/** Saves a draft of the input value when used in a form */
shouldSaveDraft: PropTypes.bool,

/** ID of the wrapping form */
formID: PropTypes.string,

...baseTextInputPropTypes,
};

Expand All @@ -50,9 +57,28 @@ 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 DatePicker({forwardedRef, containerStyles, defaultValue, disabled, errorText, inputID, isSmallScreenWidth, label, maxDate, minDate, onInputChange, onTouched, placeholder, value}) {
function DatePicker({
forwardedRef,
containerStyles,
defaultValue,
disabled,
errorText,
inputID,
isSmallScreenWidth,
label,
maxDate,
minDate,
onInputChange,
onTouched,
placeholder,
value,
shouldSaveDraft,
formID,
}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [selectedDate, setSelectedDate] = useState(value || defaultValue || undefined);
Expand All @@ -67,6 +93,19 @@ function DatePicker({forwardedRef, containerStyles, defaultValue, disabled, erro
setSelectedDate(newValue);
};

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);
}, [formID, inputID, selectedDate, shouldSaveDraft, value]);

return (
<View style={styles.datePickerRoot}>
<View style={[isSmallScreenWidth ? styles.flex2 : {}, styles.pointerEventsNone]}>
Expand Down
Loading

0 comments on commit b49c384

Please sign in to comment.