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

Migrate addplaidbankaccount.js to function component #24410

Merged
Changes from all 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
250 changes: 119 additions & 131 deletions src/components/AddPlaidBankAccount.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _ from 'underscore';
import React from 'react';
import React, {useEffect, useRef, useCallback} from 'react';
import {ActivityIndicator, View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
Expand All @@ -10,17 +10,16 @@ import * as BankAccounts from '../libs/actions/BankAccounts';
import ONYXKEYS from '../ONYXKEYS';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
import compose from '../libs/compose';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import Picker from './Picker';
import {plaidDataPropTypes} from '../pages/ReimbursementAccount/plaidDataPropTypes';
import Text from './Text';
import getBankIcon from './Icon/BankIcons';
import Icon from './Icon';
import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlockingView';
import {withNetwork} from './OnyxProvider';
import CONST from '../CONST';
import KeyboardShortcut from '../libs/KeyboardShortcut';
import useLocalize from '../hooks/useLocalize';
import useNetwork from '../hooks/useNetwork';

const propTypes = {
/** Contains plaid data */
Expand Down Expand Up @@ -52,8 +51,6 @@ const propTypes = {

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

...withLocalizePropTypes,
};

const defaultProps = {
Expand All @@ -68,172 +65,163 @@ const defaultProps = {
bankAccountID: 0,
};

class AddPlaidBankAccount extends React.Component {
constructor(props) {
super(props);

this.getPlaidLinkToken = this.getPlaidLinkToken.bind(this);
this.subscribedKeyboardShortcuts = [];
}

componentDidMount() {
this.subscribeToNavigationShortcuts();

// If we're coming from Plaid OAuth flow then we need to reuse the existing plaidLinkToken
if (this.isAuthenticatedWithPlaid()) {
return;
}

BankAccounts.openPlaidBankLogin(this.props.allowDebit, this.props.bankAccountID);
}

componentDidUpdate(prevProps) {
if (!prevProps.network.isOffline || this.props.network.isOffline || this.isAuthenticatedWithPlaid()) {
return;
}

// If we are coming back from offline and we haven't authenticated with Plaid yet, we need to re-run our call to kick off Plaid
BankAccounts.openPlaidBankLogin(this.props.allowDebit, this.props.bankAccountID);
}
function AddPlaidBankAccount({plaidData, selectedPlaidAccountID, plaidLinkToken, onExitPlaid, onSelect, text, receivedRedirectURI, plaidLinkOAuthToken, bankAccountID, allowDebit}) {
const subscribedKeyboardShortcuts = useRef([]);
const previousNetworkState = useRef();

componentWillUnmount() {
this.unsubscribeToNavigationShortcuts();
}
const {translate} = useLocalize();
const {isOffline} = useNetwork();

/**
* @returns {String}
*/
getPlaidLinkToken() {
if (this.props.plaidLinkToken) {
return this.props.plaidLinkToken;
const getPlaidLinkToken = () => {
if (plaidLinkToken) {
return plaidLinkToken;
}

if (this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) {
return this.props.plaidLinkOAuthToken;
if (receivedRedirectURI && plaidLinkOAuthToken) {
return plaidLinkOAuthToken;
}
}
};

/**
* @returns {Boolean}
* I'm using useCallback so the useEffect which uses this function doesn't run on every render.
*/
isAuthenticatedWithPlaid() {
return (
(this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) ||
!_.isEmpty(lodashGet(this.props.plaidData, 'bankAccounts')) ||
!_.isEmpty(lodashGet(this.props.plaidData, 'errors'))
);
}
const isAuthenticatedWithPlaid = useCallback(
() => (receivedRedirectURI && plaidLinkOAuthToken) || !_.isEmpty(lodashGet(plaidData, 'bankAccounts')) || !_.isEmpty(lodashGet(plaidData, 'errors')),
[plaidData, plaidLinkOAuthToken, receivedRedirectURI],
);

/**
* Blocks the keyboard shortcuts that can navigate
*/
subscribeToNavigationShortcuts() {
const subscribeToNavigationShortcuts = () => {
// find and block the shortcuts
const shortcutsToBlock = _.filter(CONST.KEYBOARD_SHORTCUTS, (x) => x.type === CONST.KEYBOARD_SHORTCUTS_TYPES.NAVIGATION_SHORTCUT);
this.subscribedKeyboardShortcuts = _.map(shortcutsToBlock, (shortcut) =>
subscribedKeyboardShortcuts.current = _.map(shortcutsToBlock, (shortcut) =>
KeyboardShortcut.subscribe(
shortcut.shortcutKey,
() => {}, // do nothing
shortcut.descriptionKey,
shortcut.modifiers,
false,
() => lodashGet(this.props.plaidData, 'bankAccounts', []).length > 0, // start bubbling when there are bank accounts
() => lodashGet(plaidData, 'bankAccounts', []).length > 0, // start bubbling when there are bank accounts
),
);
}
};

/**
* Unblocks the keyboard shortcuts that can navigate
*/
unsubscribeToNavigationShortcuts() {
_.each(this.subscribedKeyboardShortcuts, (unsubscribe) => unsubscribe());
this.subscribedKeyboardShortcuts = [];
}
const unsubscribeToNavigationShortcuts = () => {
_.each(subscribedKeyboardShortcuts.current, (unsubscribe) => unsubscribe());
subscribedKeyboardShortcuts.current = [];
};

render() {
const plaidBankAccounts = lodashGet(this.props.plaidData, 'bankAccounts') || [];
const token = this.getPlaidLinkToken();
const options = _.map(plaidBankAccounts, (account) => ({
value: account.plaidAccountID,
label: `${account.addressName} ${account.mask}`,
}));
const {icon, iconSize} = getBankIcon();
const plaidErrors = lodashGet(this.props.plaidData, 'errors');
const plaidDataErrorMessage = !_.isEmpty(plaidErrors) ? _.chain(plaidErrors).values().first().value() : '';
const bankName = lodashGet(this.props.plaidData, 'bankName');

// Plaid Link view
if (!plaidBankAccounts.length) {
return (
<FullPageOfflineBlockingView>
{lodashGet(this.props.plaidData, 'isLoading') && (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
<ActivityIndicator
color={themeColors.spinner}
size="large"
/>
</View>
)}
{Boolean(plaidDataErrorMessage) && <Text style={[styles.formError, styles.mh5]}>{plaidDataErrorMessage}</Text>}
{Boolean(token) && !bankName && (
<PlaidLink
token={token}
onSuccess={({publicToken, metadata}) => {
Log.info('[PlaidLink] Success!');
BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, this.props.allowDebit, this.props.bankAccountID);
}}
onError={(error) => {
Log.hmmm('[PlaidLink] Error: ', error.message);
}}
// User prematurely exited the Plaid flow
// eslint-disable-next-line react/jsx-props-no-multi-spaces
onExit={this.props.onExitPlaid}
receivedRedirectURI={this.props.receivedRedirectURI}
/>
)}
</FullPageOfflineBlockingView>
);
useEffect(() => {
subscribeToNavigationShortcuts();

// If we're coming from Plaid OAuth flow then we need to reuse the existing plaidLinkToken
if (isAuthenticatedWithPlaid()) {
return unsubscribeToNavigationShortcuts;
}
BankAccounts.openPlaidBankLogin(allowDebit, bankAccountID);
return unsubscribeToNavigationShortcuts;

// Plaid bank accounts view
// disabling this rule, as we want this to run only on the first render
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
// If we are coming back from offline and we haven't authenticated with Plaid yet, we need to re-run our call to kick off Plaid
// previousNetworkState.current also makes sure that this doesn't run on the first render.
if (previousNetworkState.current && !isOffline && !isAuthenticatedWithPlaid()) {
BankAccounts.openPlaidBankLogin(allowDebit, bankAccountID);
}
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}`,
}));
const {icon, iconSize} = getBankIcon();
const plaidErrors = lodashGet(plaidData, 'errors');
const plaidDataErrorMessage = !_.isEmpty(plaidErrors) ? _.chain(plaidErrors).values().first().value() : '';
const bankName = lodashGet(plaidData, 'bankName');

// Plaid Link view
if (!plaidBankAccounts.length) {
return (
<FullPageOfflineBlockingView>
{!_.isEmpty(this.props.text) && <Text style={[styles.mb5]}>{this.props.text}</Text>}
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mb5]}>
<Icon
src={icon}
height={iconSize}
width={iconSize}
/>
<Text style={[styles.ml3, styles.textStrong]}>{bankName}</Text>
</View>
<View style={[styles.mb5]}>
<Picker
label={this.props.translate('addPersonalBankAccountPage.chooseAccountLabel')}
onInputChange={this.props.onSelect}
items={options}
placeholder={{
value: '',
label: this.props.translate('bankAccount.chooseAnAccount'),
{lodashGet(plaidData, 'isLoading') && (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
<ActivityIndicator
color={themeColors.spinner}
size="large"
/>
</View>
)}
{Boolean(plaidDataErrorMessage) && <Text style={[styles.formError, styles.mh5]}>{plaidDataErrorMessage}</Text>}
{Boolean(token) && !bankName && (
<PlaidLink
token={token}
onSuccess={({publicToken, metadata}) => {
Log.info('[PlaidLink] Success!');
BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, allowDebit, bankAccountID);
}}
onError={(error) => {
Log.hmmm('[PlaidLink] Error: ', error.message);
}}
value={this.props.selectedPlaidAccountID}
// User prematurely exited the Plaid flow
// eslint-disable-next-line react/jsx-props-no-multi-spaces
onExit={onExitPlaid}
receivedRedirectURI={receivedRedirectURI}
/>
</View>
)}
</FullPageOfflineBlockingView>
);
}

// Plaid bank accounts view
return (
<FullPageOfflineBlockingView>
{!_.isEmpty(text) && <Text style={[styles.mb5]}>{text}</Text>}
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mb5]}>
<Icon
src={icon}
height={iconSize}
width={iconSize}
/>
<Text style={[styles.ml3, styles.textStrong]}>{bankName}</Text>
</View>
<View style={[styles.mb5]}>
<Picker
label={translate('addPersonalBankAccountPage.chooseAccountLabel')}
onInputChange={onSelect}
items={options}
placeholder={{
value: '',
label: translate('bankAccount.chooseAnAccount'),
}}
value={selectedPlaidAccountID}
/>
</View>
</FullPageOfflineBlockingView>
);
}

AddPlaidBankAccount.propTypes = propTypes;
AddPlaidBankAccount.defaultProps = defaultProps;

export default compose(
withLocalize,
withNetwork(),
withOnyx({
plaidLinkToken: {
key: ONYXKEYS.PLAID_LINK_TOKEN,
initWithStoredValues: false,
},
}),
)(AddPlaidBankAccount);
AddPlaidBankAccount.displayName = 'AddPlaidBankAccount';

export default withOnyx({
plaidLinkToken: {
key: ONYXKEYS.PLAID_LINK_TOKEN,
initWithStoredValues: false,
},
})(AddPlaidBankAccount);
Loading