diff --git a/src/ROUTES.js b/src/ROUTES.js
index ca7d80b93881..f3b856a469de 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -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: '',
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js
index b4b1fe23ff11..abd2f6b5b8cb 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.js
@@ -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 = {
@@ -82,6 +88,8 @@ const defaultProps = {
onExitPlaid: () => {},
onSubmit: () => {},
text: '',
+ receivedRedirectURI: null,
+ plaidLinkOAuthToken: '',
};
class AddPlaidBankAccount extends React.Component {
@@ -89,6 +97,7 @@ class AddPlaidBankAccount extends React.Component {
super(props);
this.selectAccount = this.selectAccount.bind(this);
+ this.getPlaidLinkToken = this.getPlaidLinkToken.bind(this);
this.state = {
selectedIndex: undefined,
@@ -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();
}
@@ -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}
*/
@@ -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)
- && (
-
-
-
- )}
- {!_.isEmpty(this.props.plaidLinkToken) && (
+ {(!token || this.props.plaidBankAccounts.loading)
+ && (
+
+
+
+ )}
+ {token && (
{
Log.info('[PlaidLink] Success!');
BankAccounts.fetchPlaidBankAccounts(publicToken, metadata.institution.name);
@@ -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 && (
diff --git a/src/components/PlaidLink/index.js b/src/components/PlaidLink/index.js
index 472e1f373872..8d51adb8a605 100644
--- a/src/components/PlaidLink/index.js
+++ b/src/components/PlaidLink/index.js
@@ -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(() => {
diff --git a/src/components/PlaidLink/plaidLinkPropTypes.js b/src/components/PlaidLink/plaidLinkPropTypes.js
index 70af75dd0836..b36dade2a782 100644
--- a/src/components/PlaidLink/plaidLinkPropTypes.js
+++ b/src/components/PlaidLink/plaidLinkPropTypes.js
@@ -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 {
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index aca2831a3e9a..4e89c80ec813 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -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,
},
},
diff --git a/src/libs/getPlaidLinkTokenParameters/index.android.js b/src/libs/getPlaidLinkTokenParameters/index.android.js
index 111d35dd1c7e..4174e2b8905b 100644
--- a/src/libs/getPlaidLinkTokenParameters/index.android.js
+++ b/src/libs/getPlaidLinkTokenParameters/index.android.js
@@ -1,3 +1,3 @@
import CONST from '../../CONST';
-export default () => ({android_name: CONST.ANDROID_PACKAGE_NAME});
+export default () => ({android_package: CONST.ANDROID_PACKAGE_NAME});
diff --git a/src/libs/getPlaidLinkTokenParameters/index.js b/src/libs/getPlaidLinkTokenParameters/index.js
index 56bf55ff188d..17a81fc9a24f 100644
--- a/src/libs/getPlaidLinkTokenParameters/index.js
+++ b/src/libs/getPlaidLinkTokenParameters/index.js
@@ -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}`};
+};
diff --git a/src/libs/getPlaidOAuthReceivedRedirectURI/index.js b/src/libs/getPlaidOAuthReceivedRedirectURI/index.js
new file mode 100644
index 000000000000..c53e78e5ea6f
--- /dev/null
+++ b/src/libs/getPlaidOAuthReceivedRedirectURI/index.js
@@ -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;
+};
diff --git a/src/libs/getPlaidOAuthReceivedRedirectURI/index.native.js b/src/libs/getPlaidOAuthReceivedRedirectURI/index.native.js
new file mode 100644
index 000000000000..461f67a0a4bc
--- /dev/null
+++ b/src/libs/getPlaidOAuthReceivedRedirectURI/index.native.js
@@ -0,0 +1 @@
+export default () => null;
diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js
index 1b66d9c09106..0736f19bca85 100644
--- a/src/pages/AddPersonalBankAccountPage.js
+++ b/src/pages/AddPersonalBankAccountPage.js
@@ -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 => (
@@ -21,10 +33,21 @@ const AddPersonalBankAccountPage = props => (
BankAccounts.addPersonalBankAccount(account, password, plaidLinkToken);
}}
onExitPlaid={Navigation.dismissModal}
+ receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()}
+ plaidLinkOAuthToken={props.plaidLinkToken}
/>
);
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);
diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js
index 2b20ed07ed4f..84f582cf2800 100644
--- a/src/pages/ReimbursementAccount/BankAccountStep.js
+++ b/src/pages/ReimbursementAccount/BankAccountStep.js
@@ -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';
@@ -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);
@@ -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 (
BankAccounts.setBankAccountSubStep(null)}
+ receivedRedirectURI={this.props.receivedRedirectURI}
+ plaidLinkOAuthToken={this.props.plaidLinkOAuthToken}
/>
)}
{subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL && (
@@ -292,6 +308,8 @@ class BankAccountStep extends React.Component {
}
BankAccountStep.propTypes = propTypes;
+BankAccountStep.defaultProps = defaultProps;
+
export default compose(
withLocalize,
withOnyx({
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
index bda68be13f57..31ccd39170f2 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
@@ -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
@@ -203,7 +204,6 @@ class ReimbursementAccountPage extends React.Component {
);
}
-
return (
@@ -211,6 +211,8 @@ class ReimbursementAccountPage extends React.Component {
)}
{currentStep === CONST.BANK_ACCOUNT.STEP.COMPANY && (
@@ -251,6 +253,9 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
+ plaidLinkToken: {
+ key: ONYXKEYS.PLAID_LINK_TOKEN,
+ },
}),
withLocalize,
)(ReimbursementAccountPage);