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

[PlaidLink OAuth] Add web redirect_uri to /bank-accounts page #6259

Merged
merged 73 commits into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
3768247
Initial testing
Nov 4, 2021
ecabb8a
Add new redirectURI param for plaidLinkToken call
Nov 4, 2021
6a7fac8
Update redirectURI param
Nov 4, 2021
7486623
Add redirect new.e page
Nov 5, 2021
b9df300
Hook up OAuth redirect with onyx plaidLinkToken
Nov 5, 2021
80c3ae0
Testing redirect_uri
Nov 7, 2021
319cd00
Add redirect PLAID route
Nov 7, 2021
a545977
Hook up OAuth to modalStack
Nov 7, 2021
a6ca4fb
Switch out PlaidLink with OAuthLink
Nov 7, 2021
c643221
Add redirect screen separate from settings
Nov 8, 2021
727a1ca
Add OAuth to AuthScreens
Nov 8, 2021
d072bc5
OAuth works using platypus oauth account
Nov 8, 2021
9709dbf
Make statedID work within addplaidbankaccount
Nov 8, 2021
3d758fc
Work with current plaidLink page
Nov 8, 2021
e9894d2
Setup new page with OAuthLink
Nov 9, 2021
1dd666f
Fill out PlaidOAuthPage
Nov 9, 2021
c50afb8
Finish attaching props to new PlaidRedirect page
Nov 9, 2021
1413ad4
Setup to reuse AddPlaidBankAccount
Nov 9, 2021
6587c11
Things work now
Nov 9, 2021
03437e6
Clean up logs
Nov 9, 2021
40523d3
Mass cleanup
Nov 9, 2021
e474ff1
Delete unneeded files
Nov 9, 2021
cf3e88a
Fix comment
Nov 9, 2021
5f1f635
Figure out variables for getting bank account
Nov 9, 2021
8644b35
Merge branch 'main' into nmurray-plaidlink-oauth-update
Nov 9, 2021
1578e17
Add no stateID error handling
Nov 9, 2021
66db02b
Fill out propType doc
Nov 9, 2021
c160146
Lint
Nov 9, 2021
87e5b2b
Test setting up new withdrawal bank account
Nov 10, 2021
3ec2790
Cleanup comments
Nov 10, 2021
7142e44
Lint
Nov 10, 2021
84e2a4f
Minor PR comments
Nov 10, 2021
aebd05c
Separate file extension specific platform
Nov 10, 2021
63e110a
Add propTypes file
Nov 10, 2021
673e3ce
Fill out native addPlaidBank
Nov 10, 2021
0b9522b
Setup using two different endpoints
Nov 10, 2021
750c159
Works with two different endpoints
Nov 10, 2021
a6f5c94
Lint and get adding bank account working
Nov 10, 2021
3702457
Delete onyx key
Nov 10, 2021
326b948
Testing navigation bug
Nov 13, 2021
9f51191
Improvement to redirectURI
Nov 13, 2021
71accec
try to switch over to using ReimbursementPage as base component for P…
Nov 19, 2021
01dad51
Cleanup, going from selecting account to CompanyStep works now
Nov 19, 2021
e2c4c99
Fix error where substep returns null and makes us go back to BankAcco…
Nov 19, 2021
72ff541
Revert BankAccountStep external setupBankAccount method
Nov 19, 2021
a254c03
Switch over to just using ReimbursementPage instead of new OAuthPlaid…
Nov 23, 2021
9ae9461
Add getOAuthReceivedRedirectURI and add it to Personal Bank Account Page
Nov 23, 2021
1d50056
Revert old changes
Nov 23, 2021
fc1095a
More cleanup
Nov 23, 2021
791dc61
Cleanup redirect URI logic and fix IdentityForm address error
Nov 23, 2021
3314dc4
Hook up BaseAddPlaidPage and delete old OAuthPlaidPage
Nov 23, 2021
2622ce5
Revert platform extensions
Nov 24, 2021
91b3238
Merge branch 'main' into nmurray-plaidlink-oauth-update
Nov 24, 2021
add0815
Fix lint warnings
Nov 24, 2021
fcf15e6
Include early return
Nov 24, 2021
842fb77
Variable name cleanup and comments
Nov 24, 2021
89d1b8e
Merge marc branch and fix conflicts
Nov 26, 2021
dfd9d7a
Test on web, desktop with complete uri
Nov 26, 2021
6798c11
Fix CONST.js conflicts
Nov 26, 2021
7c1be4f
Removed getRedirect logic out of AddPlaid, cleaned up src/libs/getPla…
Nov 26, 2021
07a0f6e
Clean up BankAccounts.js
Nov 26, 2021
e792b0c
Fix Url import
Nov 26, 2021
6842a47
Reduce condition to only using existing plaidLinkToken
Nov 29, 2021
6e68c70
Review comments, refactor existingPlaidToken prop
Nov 29, 2021
4f535b3
Merge branch 'main' into nmurray-plaidlink-oauth-update
Nov 29, 2021
3a8ac3a
Refactor ROUTE
Nov 29, 2021
36acde9
Fix export default
Nov 29, 2021
419d74e
Lint fix
Nov 29, 2021
51ec9c7
Second round of review comments
Nov 30, 2021
7d27703
Have addPersonalBankAccount subscribe to plaidLinkToken
Nov 30, 2021
34d2422
Update PlaidLink to check for receivedRedirectURI
Dec 2, 2021
5fa07db
Review comments and swap out CONST url for CONFIG url
Dec 3, 2021
44ff636
Merge with conflict
Dec 3, 2021
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
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;
};
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
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);