Skip to content

Commit 02f287e

Browse files
committed
feat: BankInfo step
1 parent 7d807c7 commit 02f287e

33 files changed

+1606
-199
lines changed

ios/Podfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ PODS:
234234
- libwebp/demux
235235
- libwebp/webp (1.2.4)
236236
- lottie-ios (4.3.3)
237-
- lottie-react-native (6.3.1):
238-
- lottie-ios (~> 4.3.0)
237+
- lottie-react-native (6.4.0):
238+
- lottie-ios (~> 4.3.3)
239239
- React-Core
240240
- MapboxCommon (23.6.0)
241241
- MapboxCoreMaps (10.14.0):
@@ -1203,7 +1203,7 @@ SPEC CHECKSUMS:
12031203
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
12041204
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
12051205
lottie-ios: 25e7b2675dad5c3ddad369ac9baab03560c5bfdd
1206-
lottie-react-native: c9f1db4f4124dcce9f8159e65d8dc6e8bcb11fb4
1206+
lottie-react-native: 3a3084faddd3891c276f23fd6e797b83f2021bbc
12071207
MapboxCommon: 4a0251dd470ee37e7fadda8e285c01921a5e1eb0
12081208
MapboxCoreMaps: eb07203bbb0b1509395db5ab89cd3ad6c2e3c04c
12091209
MapboxMaps: af50ec61a7eb3b032c3f7962c6bd671d93d2a209

src/CONST.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,30 @@ const CONST = {
165165
DOMAIN: '@expensify.sms',
166166
},
167167
BANK_ACCOUNT: {
168+
BANK_INFO_STEP: {
169+
INPUT_KEY: {
170+
BANK_ACCOUNT_ID: 'bankAccountId',
171+
ROUTING_NUMBER: 'routingNumber',
172+
ACCOUNT_NUMBER: 'accountNumber',
173+
PLAID_MASK: 'plaidMask',
174+
IS_SAVINGS: 'isSavings',
175+
BANK_NAME: 'bankName',
176+
PLAID_ACCOUNT_ID: 'plaidAccountID',
177+
PLAID_ACCESS_TOKEN: 'plaidAccessToken',
178+
},
179+
},
180+
PERSONAL_INFO_STEP: {
181+
INPUT_KEY: {
182+
FIRST_NAME: 'firstName',
183+
LAST_NAME: 'lastName',
184+
DOB: 'dob',
185+
SSN_LAST_4: 'ssnLast4',
186+
STREET: 'requestorAddressStreet',
187+
CITY: 'requestorAddressCity',
188+
STATE: 'requestorAddressState',
189+
ZIP_CODE: 'requestorAddressZipCode',
190+
},
191+
},
168192
PLAID: {
169193
ALLOWED_THROTTLED_COUNT: 2,
170194
ERROR: {
@@ -192,6 +216,7 @@ const CONST = {
192216
},
193217
SUBSTEP: {
194218
MANUAL: 'manual',
219+
PLAID: 'plaid',
195220
},
196221
VERIFICATIONS: {
197222
ERROR_MESSAGE: 'verifications.errorMessage',

src/components/AddPlaidBankAccount.js

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Icon from './Icon';
2020
import getBankIcon from './Icon/BankIcons';
2121
import Picker from './Picker';
2222
import PlaidLink from './PlaidLink';
23+
import RadioButtons from './RadioButtons';
2324
import Text from './Text';
2425

2526
const propTypes = {
@@ -55,6 +56,9 @@ const propTypes = {
5556

5657
/** Are we adding a withdrawal account? */
5758
allowDebit: PropTypes.bool,
59+
60+
/** Is displayed in new VBBA */
61+
isDisplayedInNewVBBA: PropTypes.bool,
5862
};
5963

6064
const defaultProps = {
@@ -68,6 +72,7 @@ const defaultProps = {
6872
allowDebit: false,
6973
bankAccountID: 0,
7074
isPlaidDisabled: false,
75+
isDisplayedInNewVBBA: false,
7176
};
7277

7378
function AddPlaidBankAccount({
@@ -82,9 +87,19 @@ function AddPlaidBankAccount({
8287
bankAccountID,
8388
allowDebit,
8489
isPlaidDisabled,
90+
isDisplayedInNewVBBA,
8591
}) {
92+
const plaidBankAccounts = lodashGet(plaidData, 'bankAccounts') || [];
93+
const defaultSelectedPlaidAccount = _.find(plaidBankAccounts, (account) => account.plaidAccountID === selectedPlaidAccountID);
94+
const defaultSelectedPlaidAccountID = lodashGet(defaultSelectedPlaidAccount, 'plaidAccountID', '');
95+
const defaultSelectedPlaidAccountMask = lodashGet(
96+
_.find(plaidBankAccounts, (account) => account.plaidAccountID === selectedPlaidAccountID),
97+
'mask',
98+
'',
99+
);
86100
const subscribedKeyboardShortcuts = useRef([]);
87101
const previousNetworkState = useRef();
102+
const [selectedPlaidAccountMask, setSelectedPlaidAccountMask] = React.useState(defaultSelectedPlaidAccountMask);
88103

89104
const {translate} = useLocalize();
90105
const {isOffline} = useNetwork();
@@ -160,17 +175,27 @@ function AddPlaidBankAccount({
160175
previousNetworkState.current = isOffline;
161176
}, [allowDebit, bankAccountID, isAuthenticatedWithPlaid, isOffline]);
162177

163-
const plaidBankAccounts = lodashGet(plaidData, 'bankAccounts') || [];
164178
const token = getPlaidLinkToken();
165179
const options = _.map(plaidBankAccounts, (account) => ({
166180
value: account.plaidAccountID,
167-
label: `${account.addressName} ${account.mask}`,
181+
label: account.addressName,
168182
}));
169183
const {icon, iconSize, iconStyles} = getBankIcon();
170184
const plaidErrors = lodashGet(plaidData, 'errors');
171185
const plaidDataErrorMessage = !_.isEmpty(plaidErrors) ? _.chain(plaidErrors).values().first().value() : '';
172186
const bankName = lodashGet(plaidData, 'bankName');
173187

188+
/**
189+
* @param {String} plaidAccountID
190+
*
191+
* When user selects one of plaid accounts we need to set the mask in order to display it on UI
192+
*/
193+
const handleSelectingPlaidAccount = (plaidAccountID) => {
194+
const mask = _.find(plaidBankAccounts, (account) => account.plaidAccountID === plaidAccountID).mask;
195+
setSelectedPlaidAccountMask(mask);
196+
onSelect(plaidAccountID);
197+
};
198+
174199
if (isPlaidDisabled) {
175200
return (
176201
<View>
@@ -227,6 +252,33 @@ function AddPlaidBankAccount({
227252
);
228253
}
229254

255+
if (isDisplayedInNewVBBA) {
256+
return (
257+
<FullPageOfflineBlockingView>
258+
<Text style={[styles.mb5, styles.textHeadline]}>{translate('bankAccount.chooseAnAccount')}</Text>
259+
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mb5]}>
260+
<Icon
261+
src={icon}
262+
height={iconSize}
263+
width={iconSize}
264+
/>
265+
<View>
266+
<Text style={[styles.ml3, styles.textStrong]}>{bankName}</Text>
267+
{selectedPlaidAccountMask.length > 0 && (
268+
<Text style={[styles.ml3, styles.textLabelSupporting]}>{`${translate('bankAccount.accountEnding')} ${selectedPlaidAccountMask}`}</Text>
269+
)}
270+
</View>
271+
</View>
272+
<Text style={[styles.textLabelSupporting]}>{`${translate('bankAccount.chooseAnAccountBelow')}:`}</Text>
273+
<RadioButtons
274+
items={options}
275+
defaultCheckedValue={defaultSelectedPlaidAccountID}
276+
onPress={handleSelectingPlaidAccount}
277+
/>
278+
</FullPageOfflineBlockingView>
279+
);
280+
}
281+
230282
// Plaid bank accounts view
231283
return (
232284
<FullPageOfflineBlockingView>

src/components/Form/FormWrapper.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ const propTypes = {
5656
/** Container styles */
5757
style: stylePropTypes,
5858

59+
/** Submit button container styles */
60+
// eslint-disable-next-line react/forbid-prop-types
61+
submitButtonStyles: PropTypes.arrayOf(PropTypes.object),
62+
5963
/** Custom content to display in the footer after submit button */
6064
footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
6165

@@ -74,10 +78,25 @@ const defaultProps = {
7478
scrollContextEnabled: false,
7579
footerContent: null,
7680
style: [],
81+
submitButtonStyles: [],
7782
};
7883

7984
function FormWrapper(props) {
80-
const {onSubmit, children, formState, errors, inputRefs, submitButtonText, footerContent, isSubmitButtonVisible, style, enabledWhenOffline, isSubmitActionDangerous, formID} = props;
85+
const {
86+
onSubmit,
87+
children,
88+
formState,
89+
errors,
90+
inputRefs,
91+
submitButtonText,
92+
footerContent,
93+
isSubmitButtonVisible,
94+
style,
95+
enabledWhenOffline,
96+
isSubmitActionDangerous,
97+
formID,
98+
submitButtonStyles,
99+
} = props;
81100
const formRef = useRef(null);
82101
const formContentRef = useRef(null);
83102
const errorMessage = useMemo(() => {
@@ -129,7 +148,7 @@ function FormWrapper(props) {
129148
focusInput.focus();
130149
}
131150
}}
132-
containerStyles={[styles.mh0, styles.mt5, styles.flex1]}
151+
containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...submitButtonStyles]}
133152
enabledWhenOffline={enabledWhenOffline}
134153
isSubmitActionDangerous={isSubmitActionDangerous}
135154
disablePressOnEnter
@@ -150,6 +169,7 @@ function FormWrapper(props) {
150169
isSubmitActionDangerous,
151170
isSubmitButtonVisible,
152171
onSubmit,
172+
submitButtonStyles,
153173
style,
154174
submitButtonText,
155175
],

src/components/InteractiveStepSubHeader.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import React, {forwardRef, useState, useImperativeHandle} from 'react';
2-
import PropTypes from 'prop-types';
31
import map from 'lodash/map';
2+
import PropTypes from 'prop-types';
3+
import React, {forwardRef, useImperativeHandle, useState} from 'react';
44
import {View} from 'react-native';
5-
6-
import CONST from '../CONST';
7-
import variables from '../styles/variables';
8-
import styles from '../styles/styles';
9-
import colors from '../styles/colors';
5+
import colors from '@styles/colors';
6+
import styles from '@styles/styles';
7+
import variables from '@styles/variables';
8+
import CONST from '@src/CONST';
9+
import Icon from './Icon';
1010
import * as Expensicons from './Icon/Expensicons';
1111
import PressableWithFeedback from './Pressable/PressableWithFeedback';
1212
import Text from './Text';
13-
import Icon from './Icon';
1413

1514
const propTypes = {
1615
/** List of the Route Name to navigate when the step is selected */

src/components/NewDatePicker/index.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import TextInput from '@components/TextInput';
99
import {propTypes as baseTextInputPropTypes, defaultProps as defaultBaseTextInputPropTypes} from '@components/TextInput/baseTextInputPropTypes';
1010
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
1111
import styles from '@styles/styles';
12+
import * as FormActions from '@userActions/FormActions';
1213
import CONST from '@src/CONST';
1314
import CalendarPicker from './CalendarPicker';
1415

@@ -33,6 +34,12 @@ const propTypes = {
3334
/** A maximum date of calendar to select */
3435
maxDate: PropTypes.objectOf(Date),
3536

37+
/** Saves a draft of the input value when used in a form */
38+
shouldSaveDraft: PropTypes.bool,
39+
40+
/** ID of the wrapping form */
41+
formID: PropTypes.string,
42+
3643
...withLocalizePropTypes,
3744
...baseTextInputPropTypes,
3845
};
@@ -42,17 +49,42 @@ const datePickerDefaultProps = {
4249
minDate: setYear(new Date(), CONST.CALENDAR_PICKER.MIN_YEAR),
4350
maxDate: setYear(new Date(), CONST.CALENDAR_PICKER.MAX_YEAR),
4451
value: undefined,
52+
shouldSaveDraft: false,
53+
formID: '',
4554
};
4655

47-
function NewDatePicker({containerStyles, defaultValue, disabled, errorText, inputID, isSmallScreenWidth, label, maxDate, minDate, onInputChange, onTouched, placeholder, translate, value}) {
56+
function NewDatePicker({
57+
containerStyles,
58+
defaultValue,
59+
disabled,
60+
errorText,
61+
inputID,
62+
isSmallScreenWidth,
63+
label,
64+
maxDate,
65+
minDate,
66+
onInputChange,
67+
onTouched,
68+
placeholder,
69+
translate,
70+
value,
71+
shouldSaveDraft,
72+
formID,
73+
}) {
4874
const [selectedDate, setSelectedDate] = useState(value || defaultValue || undefined);
4975

5076
useEffect(() => {
77+
// Value is provided to input via props and onChange never fires. We have to save draft manually.
78+
if (shouldSaveDraft && formID !== '') {
79+
FormActions.setDraftValues(formID, {[inputID]: selectedDate});
80+
}
81+
5182
if (selectedDate === value || _.isUndefined(value)) {
5283
return;
5384
}
85+
5486
setSelectedDate(value);
55-
}, [selectedDate, value]);
87+
}, [formID, inputID, selectedDate, shouldSaveDraft, value]);
5688

5789
useEffect(() => {
5890
if (_.isFunction(onTouched)) {

src/components/RadioButtons.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ type RadioButtonsProps = {
1212
/** List of choices to display via radio buttons */
1313
items: Choice[];
1414

15+
/** Default checked value */
16+
defaultCheckedValue?: string;
17+
1518
/** Callback to fire when selecting a radio button */
1619
onPress: (value: string) => void;
1720
};
1821

19-
function RadioButtons({items, onPress}: RadioButtonsProps) {
20-
const [checkedValue, setCheckedValue] = useState('');
22+
function RadioButtons({items, onPress, defaultCheckedValue = ''}: RadioButtonsProps) {
23+
const [checkedValue, setCheckedValue] = useState(defaultCheckedValue);
2124

25+
console.log(items, ' items');
26+
console.log(checkedValue, ' checkedValue');
2227
return (
2328
<View>
2429
{items.map((item) => (

src/hooks/useSubStep.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {useState, useRef, useCallback} from 'react';
21
import PropTypes from 'prop-types';
2+
import {useCallback, useRef, useState} from 'react';
33

44
const propTypes = {
55
/** an array of substep components */

0 commit comments

Comments
 (0)