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

[TS migration] Migrate 'AddPlaidBankAccount.js' component to TypeScript #37034

Merged
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import Log from '@libs/Log';
import {plaidDataPropTypes} from '@pages/ReimbursementAccount/plaidDataPropTypes';
import * as App from '@userActions/App';
import * as BankAccounts from '@userActions/BankAccounts';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PlaidData} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlockingView';
import FormHelpMessage from './FormHelpMessage';
import Icon from './Icon';
Expand All @@ -24,103 +23,82 @@ import PlaidLink from './PlaidLink';
import RadioButtons from './RadioButtons';
import Text from './Text';

const propTypes = {
type AddPlaidBankAccountOnyxProps = {
/** If the user has been throttled from Plaid */
isPlaidDisabled: PropTypes.bool,
isPlaidDisabled: OnyxEntry<boolean>;

/** Plaid SDK token to use to initialize the widget */
plaidLinkToken: OnyxEntry<string>;
};

type AddPlaidBankAccountProps = AddPlaidBankAccountOnyxProps & {
/** Contains plaid data */
plaidData: plaidDataPropTypes.isRequired,
plaidData: OnyxEntry<PlaidData>;

/** Selected account ID from the Picker associated with the end of the Plaid flow */
selectedPlaidAccountID: PropTypes.string,

/** Plaid SDK token to use to initialize the widget */
plaidLinkToken: PropTypes.string,
selectedPlaidAccountID?: string;

/** Fired when the user exits the Plaid flow */
onExitPlaid: PropTypes.func,
onExitPlaid?: () => void;

/** Fired when the user selects an account */
onSelect: PropTypes.func,
onSelect?: (plaidAccountID: string) => void;

/** Additional text to display */
text: PropTypes.string,
text?: string;

/** The OAuth URI + stateID needed to re-initialize the PlaidLink after the user logs into their bank */
receivedRedirectURI: PropTypes.string,
receivedRedirectURI?: string;

/** During the OAuth flow we need to use the plaidLink token that we initially connected with */
plaidLinkOAuthToken: PropTypes.string,
plaidLinkOAuthToken?: string;

/** If we're updating an existing bank account, what's its bank account ID? */
bankAccountID: PropTypes.number,
bankAccountID?: number;

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

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

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

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

const defaultProps = {
selectedPlaidAccountID: '',
plaidLinkToken: '',
onExitPlaid: () => {},
onSelect: () => {},
text: '',
receivedRedirectURI: null,
plaidLinkOAuthToken: '',
allowDebit: false,
bankAccountID: 0,
isPlaidDisabled: false,
isDisplayedInNewVBBA: false,
errorText: '',
onInputChange: () => {},
onInputChange?: (plaidAccountID: string) => void;
};

function AddPlaidBankAccount({
plaidData,
selectedPlaidAccountID,
selectedPlaidAccountID = '',
plaidLinkToken,
onExitPlaid,
onSelect,
text,
onExitPlaid = () => {},
onSelect = () => {},
text = '',
pac-guerreiro marked this conversation as resolved.
Show resolved Hide resolved
receivedRedirectURI,
plaidLinkOAuthToken,
bankAccountID,
allowDebit,
plaidLinkOAuthToken = '',
bankAccountID = 0,
allowDebit = false,
isPlaidDisabled,
isDisplayedInNewVBBA,
errorText,
onInputChange,
}) {
isDisplayedInNewVBBA = false,
errorText = '',
onInputChange = () => {},
}: AddPlaidBankAccountProps) {
pac-guerreiro marked this conversation as resolved.
Show resolved Hide resolved
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 plaidBankAccounts = plaidData?.bankAccounts ?? [];
const defaultSelectedPlaidAccount = plaidBankAccounts.find((account) => account.plaidAccountID === selectedPlaidAccountID);
const defaultSelectedPlaidAccountID = defaultSelectedPlaidAccount?.plaidAccountID ?? '';
const defaultSelectedPlaidAccountMask = plaidBankAccounts.find((account) => account.plaidAccountID === selectedPlaidAccountID)?.mask ?? '';
const subscribedKeyboardShortcuts = useRef<Array<() => void>>([]);
const previousNetworkState = useRef<boolean | undefined>();
const [selectedPlaidAccountMask, setSelectedPlaidAccountMask] = useState(defaultSelectedPlaidAccountMask);

const {translate} = useLocalize();
const {isOffline} = useNetwork();

/**
* @returns {String}
*/
const getPlaidLinkToken = () => {
const getPlaidLinkToken = (): string | undefined => {
if (plaidLinkToken) {
return plaidLinkToken;
}
Expand All @@ -135,7 +113,7 @@ function AddPlaidBankAccount({
* I'm using useCallback so the useEffect which uses this function doesn't run on every render.
*/
const isAuthenticatedWithPlaid = useCallback(
() => (receivedRedirectURI && plaidLinkOAuthToken) || !_.isEmpty(lodashGet(plaidData, 'bankAccounts')) || !_.isEmpty(lodashGet(plaidData, 'errors')),
() => (!!receivedRedirectURI && !!plaidLinkOAuthToken) || !plaidData?.bankAccounts?.length || !isEmptyObject(plaidData?.errors),
[plaidData, plaidLinkOAuthToken, receivedRedirectURI],
);

Expand All @@ -144,15 +122,15 @@ function AddPlaidBankAccount({
*/
const subscribeToNavigationShortcuts = () => {
// find and block the shortcuts
const shortcutsToBlock = _.filter(CONST.KEYBOARD_SHORTCUTS, (x) => x.type === CONST.KEYBOARD_SHORTCUTS_TYPES.NAVIGATION_SHORTCUT);
subscribedKeyboardShortcuts.current = _.map(shortcutsToBlock, (shortcut) =>
const shortcutsToBlock = Object.values(CONST.KEYBOARD_SHORTCUTS).filter((shortcut) => 'type' in shortcut && shortcut.type === CONST.KEYBOARD_SHORTCUTS_TYPES.NAVIGATION_SHORTCUT);
subscribedKeyboardShortcuts.current = shortcutsToBlock.map((shortcut) =>
KeyboardShortcut.subscribe(
shortcut.shortcutKey,
() => {}, // do nothing
shortcut.descriptionKey,
shortcut.modifiers,
false,
() => lodashGet(plaidData, 'bankAccounts', []).length > 0, // start bubbling when there are bank accounts
() => (plaidData?.bankAccounts ?? []).length > 0, // start bubbling when there are bank accounts
),
);
};
Expand All @@ -161,7 +139,7 @@ function AddPlaidBankAccount({
* Unblocks the keyboard shortcuts that can navigate
*/
const unsubscribeToNavigationShortcuts = () => {
_.each(subscribedKeyboardShortcuts.current, (unsubscribe) => unsubscribe());
subscribedKeyboardShortcuts.current.forEach((unsubscribe) => unsubscribe());
subscribedKeyboardShortcuts.current = [];
};

Expand Down Expand Up @@ -189,22 +167,21 @@ function AddPlaidBankAccount({
}, [allowDebit, bankAccountID, isAuthenticatedWithPlaid, isOffline]);

const token = getPlaidLinkToken();
const options = _.map(plaidBankAccounts, (account) => ({
const options = plaidBankAccounts.map((account) => ({
value: account.plaidAccountID,
label: account.addressName,
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');
const plaidErrors = plaidData?.errors;
const plaidDataErrorMessage = !isEmptyObject(plaidErrors) ? (Object.values(plaidErrors)[0] as string) : '';
const bankName = 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;
const handleSelectingPlaidAccount = (plaidAccountID: string) => {
const mask = plaidBankAccounts.find((account) => account.plaidAccountID === plaidAccountID)?.mask ?? '';
setSelectedPlaidAccountMask(mask);
onSelect(plaidAccountID);
onInputChange(plaidAccountID);
Expand All @@ -219,24 +196,24 @@ function AddPlaidBankAccount({
}

const renderPlaidLink = () => {
if (Boolean(token) && !bankName) {
if (!!token && !bankName) {
return (
<PlaidLink
token={token}
onSuccess={({publicToken, metadata}) => {
Log.info('[PlaidLink] Success!');
BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, allowDebit, bankAccountID);
BankAccounts.openPlaidBankAccountSelector(publicToken, metadata?.institution?.name ?? '', allowDebit, bankAccountID);
}}
onError={(error) => {
Log.hmmm('[PlaidLink] Error: ', error.message);
Log.hmmm('[PlaidLink] Error: ', error?.message);
}}
onEvent={(event, metadata) => {
BankAccounts.setPlaidEvent(event);
// Handle Plaid login errors (will potentially reset plaid token and item depending on the error)
if (event === 'ERROR') {
Log.hmmm('[PlaidLink] Error: ', metadata);
if (bankAccountID && metadata && metadata.error_code) {
BankAccounts.handlePlaidError(bankAccountID, metadata.error_code, metadata.error_message, metadata.request_id);
Log.hmmm('[PlaidLink] Error: ', {...metadata});
if (bankAccountID && metadata && 'error_code' in metadata) {
BankAccounts.handlePlaidError(bankAccountID, metadata.error_code ?? '', metadata.error_message ?? '', metadata.request_id);
}
}

Expand All @@ -257,7 +234,7 @@ function AddPlaidBankAccount({
return <Text style={[styles.formError, styles.mh5]}>{plaidDataErrorMessage}</Text>;
}

if (lodashGet(plaidData, 'isLoading')) {
if (plaidData?.isLoading) {
return (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
<ActivityIndicator
Expand All @@ -280,7 +257,7 @@ function AddPlaidBankAccount({
return (
<FullPageOfflineBlockingView>
<Text style={[styles.mb3, styles.textHeadline]}>{translate('bankAccount.chooseAnAccount')}</Text>
{!_.isEmpty(text) && <Text style={[styles.mb6, styles.textSupporting]}>{text}</Text>}
{!!text && <Text style={[styles.mb6, styles.textSupporting]}>{text}</Text>}
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mb6]}>
<Icon
src={icon}
Expand Down Expand Up @@ -310,7 +287,7 @@ function AddPlaidBankAccount({
// Plaid bank accounts view
return (
<FullPageOfflineBlockingView>
{!_.isEmpty(text) && <Text style={[styles.mb5]}>{text}</Text>}
{!!text && <Text style={[styles.mb5]}>{text}</Text>}
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mb5]}>
<Icon
src={icon}
Expand All @@ -336,11 +313,9 @@ function AddPlaidBankAccount({
);
}

AddPlaidBankAccount.propTypes = propTypes;
AddPlaidBankAccount.defaultProps = defaultProps;
AddPlaidBankAccount.displayName = 'AddPlaidBankAccount';

export default withOnyx({
export default withOnyx<AddPlaidBankAccountProps, AddPlaidBankAccountOnyxProps>({
plaidLinkToken: {
key: ONYXKEYS.PLAID_LINK_TOKEN,
initWithStoredValues: false,
Expand Down
4 changes: 3 additions & 1 deletion src/components/Form/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {ComponentType, FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react';
import type {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputSubmitEditingEventData, ViewStyle} from 'react-native';
import type {ValueOf} from 'type-fest';
import AddPlaidBankAccount from '@components/AddPlaidBankAccount';

Check failure on line 4 in src/components/Form/types.ts

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`
import type AddressSearch from '@components/AddressSearch';
import type AmountForm from '@components/AmountForm';
import type AmountTextInput from '@components/AmountTextInput';
Expand Down Expand Up @@ -38,7 +39,8 @@
| typeof StatePicker
| typeof RoomNameInput
| typeof ValuePicker
| typeof RadioButtons;
| typeof RadioButtons
| typeof AddPlaidBankAccount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's adjust the comment above and remove this:

 * TODO: Add remaining inputs here once these components are migrated to Typescript:
 * EmojiPickerButtonDropdown

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blazejkustra that component is not merged yet

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol, I thought comment referenced AddPlaidBankAccount, sorry!


type ValueTypeKey = 'string' | 'boolean' | 'date';
type ValueTypeMap = {
Expand Down
4 changes: 2 additions & 2 deletions src/libs/KeyboardShortcut/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,10 @@ function getPlatformEquivalentForKeys(keys: ShortcutModifiers): string[] {
function subscribe(
key: string,
callback: (event?: KeyboardEvent) => void,
descriptionKey: string,
descriptionKey: string | null,
modifiers: ShortcutModifiers = ['CTRL'],
captureOnInputs = false,
shouldBubble = false,
shouldBubble: boolean | (() => boolean) = false,
priority = 0,
shouldPreventDefault = true,
excludedNodes: string[] = [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type GetPlaidOAuthReceivedRedirectURI from './types';

const getPlaidOAuthReceivedRedirectURI: GetPlaidOAuthReceivedRedirectURI = () => null;
const getPlaidOAuthReceivedRedirectURI: GetPlaidOAuthReceivedRedirectURI = () => undefined;

export default getPlaidOAuthReceivedRedirectURI;
2 changes: 1 addition & 1 deletion src/libs/getPlaidOAuthReceivedRedirectURI/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const getPlaidOAuthReceivedRedirectURI: GetPlaidOAuthReceivedRedirectURI = () =>

// If no stateID passed in then we are either not in OAuth flow or flow is broken
if (!oauthStateID) {
return null;
return undefined;
}
return receivedRedirectURI;
};
Expand Down
2 changes: 1 addition & 1 deletion src/libs/getPlaidOAuthReceivedRedirectURI/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
type GetPlaidOAuthReceivedRedirectURI = () => null | string;
type GetPlaidOAuthReceivedRedirectURI = () => undefined | string;

export default GetPlaidOAuthReceivedRedirectURI;
3 changes: 0 additions & 3 deletions src/pages/ReimbursementAccount/BankInfo/substeps/Plaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import useThemeStyles from '@hooks/useThemeStyles';
import * as BankAccounts from '@userActions/BankAccounts';
import * as ReimbursementAccountActions from '@userActions/ReimbursementAccount';
import CONST from '@src/CONST';

Check failure on line 14 in src/pages/ReimbursementAccount/BankInfo/substeps/Plaid.tsx

View workflow job for this annotation

GitHub Actions / lint

'CONST' is defined but never used
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReimbursementAccountForm} from '@src/types/form';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
Expand Down Expand Up @@ -88,21 +88,18 @@
isSubmitButtonVisible={(plaidData?.bankAccounts ?? []).length > 0}
>
<InputWrapper
// @ts-expect-error TODO: Remove this once AddPlaidBankAccount (https://github.com/Expensify/App/issues/25119) is migrated to TypeScript
InputComponent={AddPlaidBankAccount}
text={translate('bankAccount.plaidBodyCopy')}
onSelect={(plaidAccountID: string) => {
ReimbursementAccountActions.updateReimbursementAccountDraft({plaidAccountID});
}}
plaidData={plaidData}
onExitPlaid={() => BankAccounts.setBankAccountSubStep(null)}

Check failure on line 97 in src/pages/ReimbursementAccount/BankInfo/substeps/Plaid.tsx

View workflow job for this annotation

GitHub Actions / lint

Promise-returning function provided to attribute where a void return was expected
allowDebit
bankAccountID={bankAccountID}
selectedPlaidAccountID={selectedPlaidAccountID}
isDisplayedInNewVBBA
inputID={BANK_INFO_STEP_KEYS.SELECTED_PLAID_ACCOUNT_ID}
inputMode={CONST.INPUT_MODE.TEXT}
style={[styles.mt5]}
defaultValue={selectedPlaidAccountID}
/>
</FormProvider>
Expand Down
Loading