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 8 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
13 changes: 13 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,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 @@ -207,6 +219,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
1 change: 0 additions & 1 deletion src/components/Form/FormWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ function FormWrapper(props) {
isSubmitActionDangerous,
isSubmitButtonVisible,
onSubmit,
submitButtonStyles,
style,
submitButtonStyles,
submitButtonText,
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 @@ -1139,8 +1139,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 @@ -1153,8 +1153,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
136 changes: 136 additions & 0 deletions src/pages/ReimbursementAccount/BankInfo/BankInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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(
Number(getDefaultValueForReimbursementAccountField(reimbursementAccount, bankInfoStepKeys.BANK_ACCOUNT_ID, '0')),
values[bankInfoStepKeys.ACCOUNT_NUMBER],
values[bankInfoStepKeys.ROUTING_NUMBER],
values[bankInfoStepKeys.PLAID_MASK],
);
} else if (setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID) {
BankAccounts.connectBankAccountWithPlaid(Number(getDefaultValueForReimbursementAccountField(reimbursementAccount, bankInfoStepKeys.BANK_ACCOUNT_ID, '0')), {
[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('bankAccount.bankInfo')}
/>
<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