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

feat: BankInfo step #31121

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1203,7 +1203,7 @@ SPEC CHECKSUMS:
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
lottie-ios: 25e7b2675dad5c3ddad369ac9baab03560c5bfdd
lottie-react-native: c9f1db4f4124dcce9f8159e65d8dc6e8bcb11fb4
lottie-react-native: 3a3084faddd3891c276f23fd6e797b83f2021bbc
MapboxCommon: 4a0251dd470ee37e7fadda8e285c01921a5e1eb0
MapboxCoreMaps: eb07203bbb0b1509395db5ab89cd3ad6c2e3c04c
MapboxMaps: af50ec61a7eb3b032c3f7962c6bd671d93d2a209
Expand Down
13 changes: 13 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ const CONST = {
DOMAIN: '@expensify.sms',
},
BANK_ACCOUNT: {
BANK_INFO_STEP: {
INPUT_KEY: {
BANK_ACCOUNT_ID: 'bankAccountId',
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',
Expand Down Expand Up @@ -204,6 +216,7 @@ const CONST = {
},
SUBSTEP: {
MANUAL: 'manual',
PLAID: 'plaid',
},
VERIFICATIONS: {
ERROR_MESSAGE: 'verifications.errorMessage',
Expand Down
58 changes: 55 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 @@ -20,6 +20,7 @@ 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 +56,9 @@ const propTypes = {

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

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

const defaultProps = {
Expand All @@ -68,6 +72,7 @@ const defaultProps = {
allowDebit: false,
bankAccountID: 0,
isPlaidDisabled: false,
isDisplayedInNewVBBA: false,
};

function AddPlaidBankAccount({
Expand All @@ -82,9 +87,19 @@ function AddPlaidBankAccount({
bankAccountID,
allowDebit,
isPlaidDisabled,
isDisplayedInNewVBBA,
}) {
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 @@ -160,17 +175,27 @@ 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();
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);
};

if (isPlaidDisabled) {
return (
<View>
Expand Down Expand Up @@ -227,6 +252,33 @@ function AddPlaidBankAccount({
);
}

if (isDisplayedInNewVBBA) {
return (
<FullPageOfflineBlockingView>
<Text style={[styles.mb5, styles.textHeadline]}>{translate('bankAccount.chooseAnAccount')}</Text>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mb5]}>
<Icon
src={icon}
height={iconSize}
width={iconSize}
/>
<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}
/>
</FullPageOfflineBlockingView>
);
}

// Plaid bank accounts view
return (
<FullPageOfflineBlockingView>
Expand Down
7 changes: 5 additions & 2 deletions src/components/RadioButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ type RadioButtonsProps = {
/** List of choices to display via radio buttons */
items: Choice[];

/** Default checked value */
defaultCheckedValue?: string;

/** Callback to fire when selecting a radio button */
onPress: (value: string) => void;
};

function RadioButtons({items, onPress}: RadioButtonsProps) {
const [checkedValue, setCheckedValue] = useState('');
function RadioButtons({items, onPress, defaultCheckedValue = ''}: RadioButtonsProps) {
const [checkedValue, setCheckedValue] = useState(defaultCheckedValue);

return (
<View>
Expand Down
8 changes: 8 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1141,8 +1141,16 @@ export default {
return result;
},
bankAccount: {
bankInfo: 'Bank info',
confirmBankInfo: 'Confirm bank info',
manuallyAdd: 'Manually add your bank account',
letsDoubleCheck: "Let's double check that everything looks right.",
accountEnding: 'Account ending in',
thisBankAccount: 'This bank account will be used for business payments on your workspace',
connectDifferentAccount: 'Connect a different account',
accountNumber: 'Account number',
routingNumber: 'Routing number',
chooseAnAccountBelow: 'Choose an account below',
addBankAccount: 'Add bank account',
chooseAnAccount: 'Choose an account',
connectOnlineWithPlaid: 'Connect online with Plaid',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1155,8 +1155,16 @@ export default {
return result;
},
bankAccount: {
bankInfo: 'Información bancaria',
confirmBankInfo: 'Confirmar información bancaria',
manuallyAdd: 'Agregar manualmente tu cuenta bancaria',
letsDoubleCheck: 'Verifiquemos que todo esté correcto.',
accountEnding: 'Cuenta terminada en',
thisBankAccount: 'Esta cuenta bancaria se utilizará para pagos comerciales en tu espacio de trabajo',
connectDifferentAccount: 'Conectar una cuenta diferente',
accountNumber: 'Número de cuenta',
routingNumber: 'Número de ruta',
chooseAnAccountBelow: 'Elige una cuenta a continuación',
addBankAccount: 'Añadir cuenta bancaria',
chooseAnAccount: 'Elige una cuenta',
connectOnlineWithPlaid: 'Conéctate a Plaid online',
Expand Down
6 changes: 6 additions & 0 deletions src/pages/ReimbursementAccount/BankAccountStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import BankAccountManualStep from './BankAccountManualStep';
import BankAccountPlaidStep from './BankAccountPlaidStep';
import BankInfo from './BankInfo/BankInfo';
import StepPropTypes from './StepPropTypes';

const propTypes = {
Expand Down Expand Up @@ -76,6 +77,10 @@ function BankAccountStep(props) {
ROUTES.WORKSPACE_INITIAL.getRoute(props.policyID),
)}`;

if (subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID || subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL) {
return <BankInfo />;
}

if (subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL) {
return (
<BankAccountManualStep
Expand All @@ -98,6 +103,7 @@ function BankAccountStep(props) {
);
}

// TODO Move initial screen where you select setup type to new ReimbursementAccount page as the begining of whole flow; also cleanup once this is done
return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
Expand Down
138 changes: 138 additions & 0 deletions src/pages/ReimbursementAccount/BankInfo/BankInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
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 getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI';
import reimbursementAccountDraftPropTypes from '@pages/ReimbursementAccount/ReimbursementAccountDraftPropTypes';
import {reimbursementAccountPropTypes} from '@pages/ReimbursementAccount/reimbursementAccountPropTypes';
import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes';
import getDefaultValueForReimbursementAccountField from '@pages/ReimbursementAccount/utils/getDefaultValueForReimbursementAccountField';
import getInitialSubstepForBankInfo from '@pages/ReimbursementAccount/utils/getInitialSubstepForBankInfo';
import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues';
import styles from '@styles/styles';
import * as BankAccounts from '@userActions/BankAccounts';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import Confirmation from './substeps/Confirmation';
import Manual from './substeps/Manual';
import Plaid from './substeps/Plaid';

const propTypes = {
/** Plaid SDK token to use to initialize the widget */
plaidLinkToken: PropTypes.string,

/** Reimbursement account from ONYX */
reimbursementAccount: reimbursementAccountPropTypes,

/** The draft values of the bank account being setup */
reimbursementAccountDraft: reimbursementAccountDraftPropTypes,
};

const defaultProps = {
plaidLinkToken: '',
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 bankInfoStepKeys = CONST.BANK_ACCOUNT.BANK_INFO_STEP.INPUT_KEY;
const manualSubsteps = [Manual, Confirmation];
const plaidSubsteps = [Plaid, Confirmation];
const receivedRedirectURI = getPlaidOAuthReceivedRedirectURI();

function BankInfo({reimbursementAccount, reimbursementAccountDraft, plaidLinkToken}) {
const {translate} = useLocalize();

const values = useMemo(() => getSubstepValues(bankInfoStepKeys, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]);

let setupType = getDefaultValueForReimbursementAccountField(reimbursementAccount, 'subStep');

const shouldReinitializePlaidLink = plaidLinkToken && receivedRedirectURI && setupType !== CONST.BANK_ACCOUNT.SUBSTEP.MANUAL;
if (shouldReinitializePlaidLink) {
setupType = CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID;
}

const startFrom = useMemo(() => getInitialSubstepForBankInfo(values, setupType), [setupType, values]);

const submit = useCallback(() => {
if (setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL) {
BankAccounts.connectBankAccountManually(
lodashGet(reimbursementAccount, 'achData', bankInfoStepKeys.BANK_ACCOUNT_ID) || 0,
MrMuzyk marked this conversation as resolved.
Show resolved Hide resolved
values[bankInfoStepKeys.ACCOUNT_NUMBER],
values[bankInfoStepKeys.ROUTING_NUMBER],
values[bankInfoStepKeys.PLAID_MASK],
);
} else if (setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID) {
const bankAccountID = lodashGet(reimbursementAccount, 'achData.bankAccountID') || 0;
MrMuzyk marked this conversation as resolved.
Show resolved Hide resolved
BankAccounts.connectBankAccountWithPlaid(bankAccountID, {
[bankInfoStepKeys.ROUTING_NUMBER]: values[bankInfoStepKeys.ROUTING_NUMBER],
[bankInfoStepKeys.ACCOUNT_NUMBER]: values[bankInfoStepKeys.ACCOUNT_NUMBER],
[bankInfoStepKeys.PLAID_MASK]: values[bankInfoStepKeys.PLAID_MASK],
[bankInfoStepKeys.IS_SAVINGS]: values[bankInfoStepKeys.IS_SAVINGS],
[bankInfoStepKeys.BANK_NAME]: values[bankInfoStepKeys.BANK_NAME],
[bankInfoStepKeys.PLAID_ACCOUNT_ID]: values[bankInfoStepKeys.PLAID_ACCOUNT_ID],
[bankInfoStepKeys.PLAID_ACCESS_TOKEN]: values[bankInfoStepKeys.PLAID_ACCESS_TOKEN],
});
}
}, [reimbursementAccount, setupType, values]);

const bodyContent = setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID ? plaidSubsteps : manualSubsteps;
const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo} = useSubStep({bodyContent, startFrom, onFinished: submit});

const handleBackButtonPress = () => {
if (screenIndex === 0) {
// TODO replace it with navigation to ReimbursementAccountPage once base is updated
BankAccounts.setBankAccountSubStep(null);
} else {
prevScreen();
}
};

return (
<ScreenWrapper testID={BankInfo.displayName}>
<HeaderWithBackButton
shouldShowBackButton={!(setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID && screenIndex === 0)}
onBackButtonPress={handleBackButtonPress}
title={translate('personalInfoStep.personalInfo')}
/>
<View style={[styles.ph5, styles.mv3, {height: STEPS_HEADER_HEIGHT}]}>
<InteractiveStepSubHeader
onStepSelected={() => {}}
// TODO Will be replaced with proper values
startStep={0}
stepNames={STEP_NAMES}
/>
</View>
<SubStep
isEditing={isEditing}
onNext={nextScreen}
onMove={moveTo}
/>
</ScreenWrapper>
);
}

BankInfo.propTypes = propTypes;
BankInfo.defaultProps = defaultProps;
BankInfo.displayName = 'BankInfo';

export default withOnyx({
reimbursementAccount: {
key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
},
reimbursementAccountDraft: {
key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT,
},
plaidLinkToken: {
key: ONYXKEYS.PLAID_LINK_TOKEN,
},
})(BankInfo);
Loading
Loading