diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.tsx similarity index 71% rename from src/components/AddPlaidBankAccount.js rename to src/components/AddPlaidBankAccount.tsx index b6fc639546a8..e64c95325fae 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.tsx @@ -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'; @@ -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; + /** Plaid SDK token to use to initialize the widget */ + plaidLinkToken: OnyxEntry; +}; + +type AddPlaidBankAccountProps = AddPlaidBankAccountOnyxProps & { /** Contains plaid data */ - plaidData: plaidDataPropTypes.isRequired, + plaidData: OnyxEntry; /** 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 = '', receivedRedirectURI, - plaidLinkOAuthToken, - bankAccountID, - allowDebit, + plaidLinkOAuthToken = '', + bankAccountID = 0, + allowDebit = false, isPlaidDisabled, - isDisplayedInNewVBBA, - errorText, - onInputChange, -}) { + isDisplayedInNewVBBA = false, + errorText = '', + onInputChange = () => {}, +}: AddPlaidBankAccountProps) { 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 void>>([]); + const previousNetworkState = useRef(); const [selectedPlaidAccountMask, setSelectedPlaidAccountMask] = useState(defaultSelectedPlaidAccountMask); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - /** - * @returns {String} - */ - const getPlaidLinkToken = () => { + const getPlaidLinkToken = (): string | undefined => { if (plaidLinkToken) { return plaidLinkToken; } @@ -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], ); @@ -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 ), ); }; @@ -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 = []; }; @@ -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); @@ -219,24 +196,24 @@ function AddPlaidBankAccount({ } const renderPlaidLink = () => { - if (Boolean(token) && !bankName) { + if (!!token && !bankName) { return ( { 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); } } @@ -257,7 +234,7 @@ function AddPlaidBankAccount({ return {plaidDataErrorMessage}; } - if (lodashGet(plaidData, 'isLoading')) { + if (plaidData?.isLoading) { return ( {translate('bankAccount.chooseAnAccount')} - {!_.isEmpty(text) && {text}} + {!!text && {text}} - {!_.isEmpty(text) && {text}} + {!!text && {text}} ({ plaidLinkToken: { key: ONYXKEYS.PLAID_LINK_TOKEN, initWithStoredValues: false, diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 33d127308449..d45b896279c7 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -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 type AddPlaidBankAccount from '@components/AddPlaidBankAccount'; import type AddressSearch from '@components/AddressSearch'; import type AmountForm from '@components/AmountForm'; import type AmountTextInput from '@components/AmountTextInput'; @@ -41,7 +42,8 @@ type ValidInputs = | typeof RoomNameInput | typeof ValuePicker | typeof DatePicker - | typeof RadioButtons; + | typeof RadioButtons + | typeof AddPlaidBankAccount; type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country'; type ValueTypeMap = { diff --git a/src/libs/KeyboardShortcut/index.ts b/src/libs/KeyboardShortcut/index.ts index 0571f5e271ab..b349c43b5715 100644 --- a/src/libs/KeyboardShortcut/index.ts +++ b/src/libs/KeyboardShortcut/index.ts @@ -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[] = [], diff --git a/src/libs/getPlaidOAuthReceivedRedirectURI/index.native.ts b/src/libs/getPlaidOAuthReceivedRedirectURI/index.native.ts index 49660dd6f077..1b1b8256db1e 100644 --- a/src/libs/getPlaidOAuthReceivedRedirectURI/index.native.ts +++ b/src/libs/getPlaidOAuthReceivedRedirectURI/index.native.ts @@ -1,5 +1,5 @@ import type GetPlaidOAuthReceivedRedirectURI from './types'; -const getPlaidOAuthReceivedRedirectURI: GetPlaidOAuthReceivedRedirectURI = () => null; +const getPlaidOAuthReceivedRedirectURI: GetPlaidOAuthReceivedRedirectURI = () => undefined; export default getPlaidOAuthReceivedRedirectURI; diff --git a/src/libs/getPlaidOAuthReceivedRedirectURI/index.ts b/src/libs/getPlaidOAuthReceivedRedirectURI/index.ts index c140d1c3339f..38be1e39d2f1 100644 --- a/src/libs/getPlaidOAuthReceivedRedirectURI/index.ts +++ b/src/libs/getPlaidOAuthReceivedRedirectURI/index.ts @@ -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; }; diff --git a/src/libs/getPlaidOAuthReceivedRedirectURI/types.ts b/src/libs/getPlaidOAuthReceivedRedirectURI/types.ts index b89f023e05d3..fd4790c38caf 100644 --- a/src/libs/getPlaidOAuthReceivedRedirectURI/types.ts +++ b/src/libs/getPlaidOAuthReceivedRedirectURI/types.ts @@ -1,3 +1,3 @@ -type GetPlaidOAuthReceivedRedirectURI = () => null | string; +type GetPlaidOAuthReceivedRedirectURI = () => undefined | string; export default GetPlaidOAuthReceivedRedirectURI; diff --git a/src/pages/ReimbursementAccount/BankInfo/substeps/Plaid.tsx b/src/pages/ReimbursementAccount/BankInfo/substeps/Plaid.tsx index a1a609f13734..224afd8c9344 100644 --- a/src/pages/ReimbursementAccount/BankInfo/substeps/Plaid.tsx +++ b/src/pages/ReimbursementAccount/BankInfo/substeps/Plaid.tsx @@ -11,7 +11,6 @@ import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as BankAccounts from '@userActions/BankAccounts'; import * as ReimbursementAccountActions from '@userActions/ReimbursementAccount'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReimbursementAccountForm} from '@src/types/form'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -88,21 +87,20 @@ function Plaid({reimbursementAccount, reimbursementAccountDraft, onNext, plaidDa isSubmitButtonVisible={(plaidData?.bankAccounts ?? []).length > 0} > { ReimbursementAccountActions.updateReimbursementAccountDraft({plaidAccountID}); }} plaidData={plaidData} - onExitPlaid={() => BankAccounts.setBankAccountSubStep(null)} + onExitPlaid={() => { + BankAccounts.setBankAccountSubStep(null); + }} 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} />