Skip to content

Commit

Permalink
Merge pull request #6259 from Expensify/nmurray-plaidlink-oauth-update
Browse files Browse the repository at this point in the history
[PlaidLink OAuth] Add web redirect_uri to `/bank-accounts` page
  • Loading branch information
Nicholas Murray authored Dec 3, 2021
2 parents 95cbd34 + 44ff636 commit 85799c2
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 16 deletions.
3 changes: 2 additions & 1 deletion src/ROUTES.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const IOU_BILL_CURRENCY = `${IOU_BILL}/currency`;
const IOU_SEND_CURRENCY = `${IOU_SEND}/currency`;

export default {
BANK_ACCOUNT: 'bank-account/:stepToOpen?',
BANK_ACCOUNT: 'bank-account',
BANK_ACCOUNT_WITH_STEP_TO_OPEN: 'bank-account/:stepToOpen?',
BANK_ACCOUNT_PERSONAL: 'bank-account/personal',
getBankAccountRoute: (stepToOpen = '') => `bank-account/${stepToOpen}`,
HOME: '',
Expand Down
49 changes: 40 additions & 9 deletions src/components/AddPlaidBankAccount.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ const propTypes = {

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

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

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

const defaultProps = {
Expand All @@ -82,13 +88,16 @@ const defaultProps = {
onExitPlaid: () => {},
onSubmit: () => {},
text: '',
receivedRedirectURI: null,
plaidLinkOAuthToken: '',
};

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

this.selectAccount = this.selectAccount.bind(this);
this.getPlaidLinkToken = this.getPlaidLinkToken.bind(this);

this.state = {
selectedIndex: undefined,
Expand All @@ -100,6 +109,12 @@ class AddPlaidBankAccount extends React.Component {
}

componentDidMount() {
// If we're coming from Plaid OAuth flow then we need to reuse the existing plaidLinkToken
// Otherwise, clear the existing token and fetch a new one
if (this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) {
return;
}

BankAccounts.clearPlaidBankAccountsAndToken();
BankAccounts.fetchPlaidLinkToken();
}
Expand All @@ -113,6 +128,19 @@ class AddPlaidBankAccount extends React.Component {
return lodashGet(this.props.plaidBankAccounts, 'accounts', []);
}

/**
* @returns {String}
*/
getPlaidLinkToken() {
if (!_.isEmpty(this.props.plaidLinkToken)) {
return this.props.plaidLinkToken;
}

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

/**
* @returns {Boolean}
*/
Expand All @@ -136,27 +164,29 @@ class AddPlaidBankAccount extends React.Component {
this.props.onSubmit({
bankName,
account,
plaidLinkToken: this.props.plaidLinkToken,
plaidLinkToken: this.getPlaidLinkToken(),
});
}

render() {
const accounts = this.getAccounts();
const token = this.getPlaidLinkToken();
const options = _.map(accounts, (account, index) => ({
value: index, label: `${account.addressName} ${account.accountNumber}`,
}));
const {icon, iconSize} = getBankIcon(this.state.institution.name);

return (
<>
{(!this.props.plaidLinkToken || this.props.plaidBankAccounts.loading)
&& (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
<ActivityIndicator color={themeColors.spinner} size="large" />
</View>
)}
{!_.isEmpty(this.props.plaidLinkToken) && (
{(!token || this.props.plaidBankAccounts.loading)
&& (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
<ActivityIndicator color={themeColors.spinner} size="large" />
</View>
)}
{token && (
<PlaidLink
token={this.props.plaidLinkToken}
token={token}
onSuccess={({publicToken, metadata}) => {
Log.info('[PlaidLink] Success!');
BankAccounts.fetchPlaidBankAccounts(publicToken, metadata.institution.name);
Expand All @@ -169,6 +199,7 @@ class AddPlaidBankAccount extends React.Component {
// User prematurely exited the Plaid flow
// eslint-disable-next-line react/jsx-props-no-multi-spaces
onExit={this.props.onExitPlaid}
receivedRedirectURI={this.props.receivedRedirectURI}
/>
)}
{accounts.length > 0 && (
Expand Down
4 changes: 4 additions & 0 deletions src/components/PlaidLink/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const PlaidLink = (props) => {
onEvent: (event, metadata) => {
Log.info('[PlaidLink] Event: ', false, {event, metadata});
},

// The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the
// user to their respective bank platform
receivedRedirectUri: props.receivedRedirectURI,
});

useEffect(() => {
Expand Down
5 changes: 5 additions & 0 deletions src/components/PlaidLink/plaidLinkPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ const plaidLinkPropTypes = {

// Callback to execute when the user leaves the Plaid widget flow without entering any information
onExit: PropTypes.func,

// The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the
// user to their respective bank platform
receivedRedirectURI: PropTypes.string,
};

const plaidLinkDefaultProps = {
onSuccess: () => {},
onError: () => {},
onExit: () => {},
receivedRedirectURI: null,
};

export {
Expand Down
2 changes: 1 addition & 1 deletion src/libs/Navigation/linkingConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default {
path: ROUTES.WORKSPACE_INVITE,
},
ReimbursementAccount: {
path: ROUTES.BANK_ACCOUNT,
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN,
exact: true,
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/libs/getPlaidLinkTokenParameters/index.android.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import CONST from '../../CONST';

export default () => ({android_name: CONST.ANDROID_PACKAGE_NAME});
export default () => ({android_package: CONST.ANDROID_PACKAGE_NAME});
8 changes: 7 additions & 1 deletion src/libs/getPlaidLinkTokenParameters/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export default () => ({});
import ROUTES from '../../ROUTES';
import CONFIG from '../../CONFIG';

export default () => {
const bankAccountRoute = window.location.href.includes('personal') ? ROUTES.BANK_ACCOUNT_PERSONAL : ROUTES.BANK_ACCOUNT;
return {redirect_uri: `${CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH}/${bankAccountRoute}`};
};
17 changes: 17 additions & 0 deletions src/libs/getPlaidOAuthReceivedRedirectURI/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* After a user authenticates their bank in the Plaid OAuth flow, Plaid returns us to the redirectURI we
* gave them along with a stateID param. We hand off the receivedRedirectUri to PlaidLink to finish connecting
* the user's account.
* @returns {String | null}
*/
export default () => {
const receivedRedirectURI = window.location.href;
const receivedRedirectSearchParams = (new URL(window.location.href)).searchParams;
const oauthStateID = receivedRedirectSearchParams.get('oauth_state_id');

// If no stateID passed in then we are either not in OAuth flow or flow is broken
if (!oauthStateID) {
return null;
}
return receivedRedirectURI;
};
1 change: 1 addition & 0 deletions src/libs/getPlaidOAuthReceivedRedirectURI/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => null;
25 changes: 24 additions & 1 deletion src/pages/AddPersonalBankAccountPage.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
import ScreenWrapper from '../components/ScreenWrapper';
import Navigation from '../libs/Navigation/Navigation';
import * as BankAccounts from '../libs/actions/BankAccounts';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import AddPlaidBankAccount from '../components/AddPlaidBankAccount';
import getPlaidOAuthReceivedRedirectURI from '../libs/getPlaidOAuthReceivedRedirectURI';
import compose from '../libs/compose';
import ONYXKEYS from '../ONYXKEYS';

const propTypes = {
...withLocalizePropTypes,

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

const defaultProps = {
plaidLinkToken: '',
};

const AddPersonalBankAccountPage = props => (
Expand All @@ -21,10 +33,21 @@ const AddPersonalBankAccountPage = props => (
BankAccounts.addPersonalBankAccount(account, password, plaidLinkToken);
}}
onExitPlaid={Navigation.dismissModal}
receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()}
plaidLinkOAuthToken={props.plaidLinkToken}
/>
</ScreenWrapper>
);

AddPersonalBankAccountPage.propTypes = propTypes;
AddPersonalBankAccountPage.defaultProps = defaultProps;
AddPersonalBankAccountPage.displayName = 'AddPersonalBankAccountPage';
export default withLocalize(AddPersonalBankAccountPage);

export default compose(
withLocalize,
withOnyx({
plaidLinkToken: {
key: ONYXKEYS.PLAID_LINK_TOKEN,
},
}),
)(AddPersonalBankAccountPage);
20 changes: 19 additions & 1 deletion src/pages/ReimbursementAccount/BankAccountStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import _ from 'underscore';
import React from 'react';
import {View, Image, ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import MenuItem from '../../components/MenuItem';
import * as Expensicons from '../../components/Icon/Expensicons';
Expand Down Expand Up @@ -32,9 +33,20 @@ const propTypes = {
// eslint-disable-next-line react/no-unused-prop-types
reimbursementAccount: reimbursementAccountPropTypes.isRequired,

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

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

...withLocalizePropTypes,
};

const defaultProps = {
receivedRedirectURI: null,
plaidLinkOAuthToken: '',
};

class BankAccountStep extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -159,7 +171,9 @@ class BankAccountStep extends React.Component {
// Disable bank account fields once they've been added in db so they can't be changed
const isFromPlaid = this.props.achData.setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID;
const shouldDisableInputs = Boolean(this.props.achData.bankAccountID) || isFromPlaid;
const subStep = this.props.achData.subStep;
const shouldReinitializePlaidLink = this.props.plaidLinkOAuthToken && this.props.receivedRedirectURI;
const subStep = shouldReinitializePlaidLink ? CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID : this.props.achData.subStep;

return (
<View style={[styles.flex1, styles.justifyContentBetween]}>
<HeaderWithCloseButton
Expand Down Expand Up @@ -237,6 +251,8 @@ class BankAccountStep extends React.Component {
text={this.props.translate('bankAccount.plaidBodyCopy')}
onSubmit={this.addPlaidAccount}
onExitPlaid={() => BankAccounts.setBankAccountSubStep(null)}
receivedRedirectURI={this.props.receivedRedirectURI}
plaidLinkOAuthToken={this.props.plaidLinkOAuthToken}
/>
)}
{subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL && (
Expand Down Expand Up @@ -292,6 +308,8 @@ class BankAccountStep extends React.Component {
}

BankAccountStep.propTypes = propTypes;
BankAccountStep.defaultProps = defaultProps;

export default compose(
withLocalize,
withOnyx({
Expand Down
7 changes: 6 additions & 1 deletion src/pages/ReimbursementAccount/ReimbursementAccountPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize
import compose from '../../libs/compose';
import styles from '../../styles/styles';
import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
import getPlaidOAuthReceivedRedirectURI from '../../libs/getPlaidOAuthReceivedRedirectURI';
import ExpensifyText from '../../components/ExpensifyText';

// Steps
Expand Down Expand Up @@ -203,14 +204,15 @@ class ReimbursementAccountPage extends React.Component {
</ScreenWrapper>
);
}

return (
<ScreenWrapper>
<KeyboardAvoidingView>
{currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT && (
<BankAccountStep
achData={achData}
isPlaidDisabled={this.props.reimbursementAccount.isPlaidDisabled}
receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()}
plaidLinkOAuthToken={this.props.plaidLinkToken}
/>
)}
{currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && (
Expand Down Expand Up @@ -251,6 +253,9 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
plaidLinkToken: {
key: ONYXKEYS.PLAID_LINK_TOKEN,
},
}),
withLocalize,
)(ReimbursementAccountPage);

0 comments on commit 85799c2

Please sign in to comment.