Skip to content

Commit 85799c2

Browse files
author
Nicholas Murray
authored
Merge pull request #6259 from Expensify/nmurray-plaidlink-oauth-update
[PlaidLink OAuth] Add web redirect_uri to `/bank-accounts` page
2 parents 95cbd34 + 44ff636 commit 85799c2

File tree

12 files changed

+127
-16
lines changed

12 files changed

+127
-16
lines changed

src/ROUTES.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const IOU_BILL_CURRENCY = `${IOU_BILL}/currency`;
1515
const IOU_SEND_CURRENCY = `${IOU_SEND}/currency`;
1616

1717
export default {
18-
BANK_ACCOUNT: 'bank-account/:stepToOpen?',
18+
BANK_ACCOUNT: 'bank-account',
19+
BANK_ACCOUNT_WITH_STEP_TO_OPEN: 'bank-account/:stepToOpen?',
1920
BANK_ACCOUNT_PERSONAL: 'bank-account/personal',
2021
getBankAccountRoute: (stepToOpen = '') => `bank-account/${stepToOpen}`,
2122
HOME: '',

src/components/AddPlaidBankAccount.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ const propTypes = {
7272

7373
/** Additional text to display */
7474
text: PropTypes.string,
75+
76+
/** The OAuth URI + stateID needed to re-initialize the PlaidLink after the user logs into their bank */
77+
receivedRedirectURI: PropTypes.string,
78+
79+
/** During the OAuth flow we need to use the plaidLink token that we initially connected with */
80+
plaidLinkOAuthToken: PropTypes.string,
7581
};
7682

7783
const defaultProps = {
@@ -82,13 +88,16 @@ const defaultProps = {
8288
onExitPlaid: () => {},
8389
onSubmit: () => {},
8490
text: '',
91+
receivedRedirectURI: null,
92+
plaidLinkOAuthToken: '',
8593
};
8694

8795
class AddPlaidBankAccount extends React.Component {
8896
constructor(props) {
8997
super(props);
9098

9199
this.selectAccount = this.selectAccount.bind(this);
100+
this.getPlaidLinkToken = this.getPlaidLinkToken.bind(this);
92101

93102
this.state = {
94103
selectedIndex: undefined,
@@ -100,6 +109,12 @@ class AddPlaidBankAccount extends React.Component {
100109
}
101110

102111
componentDidMount() {
112+
// If we're coming from Plaid OAuth flow then we need to reuse the existing plaidLinkToken
113+
// Otherwise, clear the existing token and fetch a new one
114+
if (this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) {
115+
return;
116+
}
117+
103118
BankAccounts.clearPlaidBankAccountsAndToken();
104119
BankAccounts.fetchPlaidLinkToken();
105120
}
@@ -113,6 +128,19 @@ class AddPlaidBankAccount extends React.Component {
113128
return lodashGet(this.props.plaidBankAccounts, 'accounts', []);
114129
}
115130

131+
/**
132+
* @returns {String}
133+
*/
134+
getPlaidLinkToken() {
135+
if (!_.isEmpty(this.props.plaidLinkToken)) {
136+
return this.props.plaidLinkToken;
137+
}
138+
139+
if (this.props.receivedRedirectURI && this.props.plaidLinkOAuthToken) {
140+
return this.props.plaidLinkOAuthToken;
141+
}
142+
}
143+
116144
/**
117145
* @returns {Boolean}
118146
*/
@@ -136,27 +164,29 @@ class AddPlaidBankAccount extends React.Component {
136164
this.props.onSubmit({
137165
bankName,
138166
account,
139-
plaidLinkToken: this.props.plaidLinkToken,
167+
plaidLinkToken: this.getPlaidLinkToken(),
140168
});
141169
}
142170

143171
render() {
144172
const accounts = this.getAccounts();
173+
const token = this.getPlaidLinkToken();
145174
const options = _.map(accounts, (account, index) => ({
146175
value: index, label: `${account.addressName} ${account.accountNumber}`,
147176
}));
148177
const {icon, iconSize} = getBankIcon(this.state.institution.name);
178+
149179
return (
150180
<>
151-
{(!this.props.plaidLinkToken || this.props.plaidBankAccounts.loading)
152-
&& (
153-
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
154-
<ActivityIndicator color={themeColors.spinner} size="large" />
155-
</View>
156-
)}
157-
{!_.isEmpty(this.props.plaidLinkToken) && (
181+
{(!token || this.props.plaidBankAccounts.loading)
182+
&& (
183+
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
184+
<ActivityIndicator color={themeColors.spinner} size="large" />
185+
</View>
186+
)}
187+
{token && (
158188
<PlaidLink
159-
token={this.props.plaidLinkToken}
189+
token={token}
160190
onSuccess={({publicToken, metadata}) => {
161191
Log.info('[PlaidLink] Success!');
162192
BankAccounts.fetchPlaidBankAccounts(publicToken, metadata.institution.name);
@@ -169,6 +199,7 @@ class AddPlaidBankAccount extends React.Component {
169199
// User prematurely exited the Plaid flow
170200
// eslint-disable-next-line react/jsx-props-no-multi-spaces
171201
onExit={this.props.onExitPlaid}
202+
receivedRedirectURI={this.props.receivedRedirectURI}
172203
/>
173204
)}
174205
{accounts.length > 0 && (

src/components/PlaidLink/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const PlaidLink = (props) => {
1818
onEvent: (event, metadata) => {
1919
Log.info('[PlaidLink] Event: ', false, {event, metadata});
2020
},
21+
22+
// The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the
23+
// user to their respective bank platform
24+
receivedRedirectUri: props.receivedRedirectURI,
2125
});
2226

2327
useEffect(() => {

src/components/PlaidLink/plaidLinkPropTypes.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ const plaidLinkPropTypes = {
1212

1313
// Callback to execute when the user leaves the Plaid widget flow without entering any information
1414
onExit: PropTypes.func,
15+
16+
// The redirect URI with an OAuth state ID. Needed to re-initialize the PlaidLink after directing the
17+
// user to their respective bank platform
18+
receivedRedirectURI: PropTypes.string,
1519
};
1620

1721
const plaidLinkDefaultProps = {
1822
onSuccess: () => {},
1923
onError: () => {},
2024
onExit: () => {},
25+
receivedRedirectURI: null,
2126
};
2227

2328
export {

src/libs/Navigation/linkingConfig.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export default {
103103
path: ROUTES.WORKSPACE_INVITE,
104104
},
105105
ReimbursementAccount: {
106-
path: ROUTES.BANK_ACCOUNT,
106+
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN,
107107
exact: true,
108108
},
109109
},
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import CONST from '../../CONST';
22

3-
export default () => ({android_name: CONST.ANDROID_PACKAGE_NAME});
3+
export default () => ({android_package: CONST.ANDROID_PACKAGE_NAME});
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
export default () => ({});
1+
import ROUTES from '../../ROUTES';
2+
import CONFIG from '../../CONFIG';
3+
4+
export default () => {
5+
const bankAccountRoute = window.location.href.includes('personal') ? ROUTES.BANK_ACCOUNT_PERSONAL : ROUTES.BANK_ACCOUNT;
6+
return {redirect_uri: `${CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH}/${bankAccountRoute}`};
7+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* After a user authenticates their bank in the Plaid OAuth flow, Plaid returns us to the redirectURI we
3+
* gave them along with a stateID param. We hand off the receivedRedirectUri to PlaidLink to finish connecting
4+
* the user's account.
5+
* @returns {String | null}
6+
*/
7+
export default () => {
8+
const receivedRedirectURI = window.location.href;
9+
const receivedRedirectSearchParams = (new URL(window.location.href)).searchParams;
10+
const oauthStateID = receivedRedirectSearchParams.get('oauth_state_id');
11+
12+
// If no stateID passed in then we are either not in OAuth flow or flow is broken
13+
if (!oauthStateID) {
14+
return null;
15+
}
16+
return receivedRedirectURI;
17+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default () => null;

src/pages/AddPersonalBankAccountPage.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {withOnyx} from 'react-native-onyx';
24
import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
35
import ScreenWrapper from '../components/ScreenWrapper';
46
import Navigation from '../libs/Navigation/Navigation';
57
import * as BankAccounts from '../libs/actions/BankAccounts';
68
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
79
import AddPlaidBankAccount from '../components/AddPlaidBankAccount';
10+
import getPlaidOAuthReceivedRedirectURI from '../libs/getPlaidOAuthReceivedRedirectURI';
11+
import compose from '../libs/compose';
12+
import ONYXKEYS from '../ONYXKEYS';
813

914
const propTypes = {
1015
...withLocalizePropTypes,
16+
17+
/** Plaid SDK token to use to initialize the widget */
18+
plaidLinkToken: PropTypes.string,
19+
};
20+
21+
const defaultProps = {
22+
plaidLinkToken: '',
1123
};
1224

1325
const AddPersonalBankAccountPage = props => (
@@ -21,10 +33,21 @@ const AddPersonalBankAccountPage = props => (
2133
BankAccounts.addPersonalBankAccount(account, password, plaidLinkToken);
2234
}}
2335
onExitPlaid={Navigation.dismissModal}
36+
receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()}
37+
plaidLinkOAuthToken={props.plaidLinkToken}
2438
/>
2539
</ScreenWrapper>
2640
);
2741

2842
AddPersonalBankAccountPage.propTypes = propTypes;
43+
AddPersonalBankAccountPage.defaultProps = defaultProps;
2944
AddPersonalBankAccountPage.displayName = 'AddPersonalBankAccountPage';
30-
export default withLocalize(AddPersonalBankAccountPage);
45+
46+
export default compose(
47+
withLocalize,
48+
withOnyx({
49+
plaidLinkToken: {
50+
key: ONYXKEYS.PLAID_LINK_TOKEN,
51+
},
52+
}),
53+
)(AddPersonalBankAccountPage);

src/pages/ReimbursementAccount/BankAccountStep.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import _ from 'underscore';
22
import React from 'react';
33
import {View, Image, ScrollView} from 'react-native';
44
import {withOnyx} from 'react-native-onyx';
5+
import PropTypes from 'prop-types';
56
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
67
import MenuItem from '../../components/MenuItem';
78
import * as Expensicons from '../../components/Icon/Expensicons';
@@ -32,9 +33,20 @@ const propTypes = {
3233
// eslint-disable-next-line react/no-unused-prop-types
3334
reimbursementAccount: reimbursementAccountPropTypes.isRequired,
3435

36+
/** The OAuth URI + stateID needed to re-initialize the PlaidLink after the user logs into their bank */
37+
receivedRedirectURI: PropTypes.string,
38+
39+
/** During the OAuth flow we need to use the plaidLink token that we initially connected with */
40+
plaidLinkOAuthToken: PropTypes.string,
41+
3542
...withLocalizePropTypes,
3643
};
3744

45+
const defaultProps = {
46+
receivedRedirectURI: null,
47+
plaidLinkOAuthToken: '',
48+
};
49+
3850
class BankAccountStep extends React.Component {
3951
constructor(props) {
4052
super(props);
@@ -159,7 +171,9 @@ class BankAccountStep extends React.Component {
159171
// Disable bank account fields once they've been added in db so they can't be changed
160172
const isFromPlaid = this.props.achData.setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID;
161173
const shouldDisableInputs = Boolean(this.props.achData.bankAccountID) || isFromPlaid;
162-
const subStep = this.props.achData.subStep;
174+
const shouldReinitializePlaidLink = this.props.plaidLinkOAuthToken && this.props.receivedRedirectURI;
175+
const subStep = shouldReinitializePlaidLink ? CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID : this.props.achData.subStep;
176+
163177
return (
164178
<View style={[styles.flex1, styles.justifyContentBetween]}>
165179
<HeaderWithCloseButton
@@ -237,6 +251,8 @@ class BankAccountStep extends React.Component {
237251
text={this.props.translate('bankAccount.plaidBodyCopy')}
238252
onSubmit={this.addPlaidAccount}
239253
onExitPlaid={() => BankAccounts.setBankAccountSubStep(null)}
254+
receivedRedirectURI={this.props.receivedRedirectURI}
255+
plaidLinkOAuthToken={this.props.plaidLinkOAuthToken}
240256
/>
241257
)}
242258
{subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL && (
@@ -292,6 +308,8 @@ class BankAccountStep extends React.Component {
292308
}
293309

294310
BankAccountStep.propTypes = propTypes;
311+
BankAccountStep.defaultProps = defaultProps;
312+
295313
export default compose(
296314
withLocalize,
297315
withOnyx({

src/pages/ReimbursementAccount/ReimbursementAccountPage.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize
1717
import compose from '../../libs/compose';
1818
import styles from '../../styles/styles';
1919
import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
20+
import getPlaidOAuthReceivedRedirectURI from '../../libs/getPlaidOAuthReceivedRedirectURI';
2021
import ExpensifyText from '../../components/ExpensifyText';
2122

2223
// Steps
@@ -203,14 +204,15 @@ class ReimbursementAccountPage extends React.Component {
203204
</ScreenWrapper>
204205
);
205206
}
206-
207207
return (
208208
<ScreenWrapper>
209209
<KeyboardAvoidingView>
210210
{currentStep === CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT && (
211211
<BankAccountStep
212212
achData={achData}
213213
isPlaidDisabled={this.props.reimbursementAccount.isPlaidDisabled}
214+
receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()}
215+
plaidLinkOAuthToken={this.props.plaidLinkToken}
214216
/>
215217
)}
216218
{currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && (
@@ -251,6 +253,9 @@ export default compose(
251253
betas: {
252254
key: ONYXKEYS.BETAS,
253255
},
256+
plaidLinkToken: {
257+
key: ONYXKEYS.PLAID_LINK_TOKEN,
258+
},
254259
}),
255260
withLocalize,
256261
)(ReimbursementAccountPage);

0 commit comments

Comments
 (0)