burstware / expo-plaid-link

Use the Plaid Link flow inside your expo app
MIT License
35 stars 21 forks source link

Support OAuth #11

Open Crizzooo opened 3 years ago

Crizzooo commented 3 years ago

Hello,

Not reporting a specific issue as much as asking a question.

Is the current webView implementation intended to work with OAuth flows? Such as Platypus in Sandbox, or Chase in production.

I believe we need to add code to be able to relaunch PlaidLink with a receivedRedirectUri.

What do you think the current expected behavior is for OAuth flows?

JBaczuk commented 3 years ago

@Crizzooo I'm not sure I understand your question. Plaid has a very specific flow, when would a different OAuth flow occur?

Crizzooo commented 3 years ago

@JBaczuk Does this currently work with OAuth that redirects to Chase app, and then back to the RN app?

valentinoConti commented 3 years ago

@JBaczuk I think it does not support OAuth, running the example of the repo, you can pay with normal payments, but if you select ACH and select the "5th Bank of Central Wisconsin" that uses OAuth, you end up with a infinite spinner with no redirect

JBaczuk commented 2 years ago

I'm not even sure this is possible, but I'll tag it as a feature request.

isaachinman commented 2 years ago

@JBaczuk Thanks very much for putting together this package – saved me a lot of headache today. I do believe OAuth could hypothetically be implemented via receivedRedirectUri, right?

Are you aware of any examples using receivedRedirectUri? I have searched extensively but haven't found anything.

valentinoConti commented 2 years ago

@JBaczuk Thanks very much for putting together this package – saved me a lot of headache today. I do believe OAuth could hypothetically be implemented via receivedRedirectUri, right?

Are you aware of any examples using receivedRedirectUri? I have searched extensively but haven't found anything.

Our app uses Expo and on internet I only found information to do it with react-native-cli and its pods. I was able to do this after a lot of struggle with Expo too!

First I had to configure the client_id and public_key and add a Redirect URI on the Plaid dashboard (dashboard.plaid.com). The redirect URI has to end on ".html" and that link has to be a static HTML file that redirects you back to the app. My HTML file looks like this:

<html>
<head>
  <style>.text { font-size: 64px; }</style>
</head>
<body style="text-align: center;">
  <p class="text">Please wait while we redirect you to MYAPP again...</p>
  <p class="text"><a href="javascript:redirectToApp()">Open MYAPP</a></p>
  <script>
    var redirectToApp = function() {
      var openURL = window.location.search + window.location.hash;
      window.location.replace(`yourapp://--/oauth${openURL}`);
    }
    window.onload = redirectToApp;
  </script>
</body>
</html>

Of course you have to add "youapp" to your scheme on your app.json file, so your app recognizes the redirect with the link that looks like yourapp://whatever, for example:

{
  "expo": {
    "name": "asd",
    "slug": "asd",
    "version": "0.1.0",
    "scheme": "yourapp",
    ...

and then handle the redirect with something like Linking.addEventListener('url', this.openDeepLink); somewhere on the root of your app (search "deep linking with expo" if you don't know about this)


Then on the react-native code, I added the following methods:

import { WebView } from 'react-native-webview';
...

const injectedJavaScript = `(function() {
  window.postMessage = function(data) {
    window.ReactNativeWebView.postMessage(data);
  };
})()`;

const webViewRef = useRef<WebView>();

const navigationRedirect = event => {
    if (event.url && event.url.startsWith('plaidlink://')) {
      const eventParams = queryString.parse(event.url.replace(/.*\?/, ''))
      const { link_session_id, institution_id, institution_name } = eventParams;

      if (event.url.startsWith('plaidlink://exit')) {
        // handle plaid exit here, for example go back on navigation.
         console.log('plaid exit!');
      } else if (event.url.startsWith('plaidlink://connected')) {
        // handle connect successfully here
        const { public_token } = eventParams;
        const accounts = JSON.parse(eventParams.accounts);
        console.log('plaid connect!');
        console.log('account_id', accounts[0]["_id"]);
        console.log('institution_id', institution_id);
        console.log('institution_name', institution_name);
        console.log('link_session_id', link_session_id);
        console.log('public_token', public_token);
      }
      return false
    }
    return true
}

and finally I used the component like this, where token its the link token that you get via plaid API sending as a minimum the redirectURI + your client id + your client name (docs here), and env its 'sandbox' or 'production' depending on the running environment.

<WebView
      ref={webViewRef}
      source={`https://cdn.plaid.com/link/v2/stable/link.html?token=${token}&env=${env}&isWebview=true`}
      useWebKit
      injectedJavaScript={injectedJavaScript}
      originWhitelist={['http://*', 'https://*', 'plaidlink://*']}
      onShouldStartLoadWithRequest={navigationRedirect}
      onNavigationStateChange={navigationRedirect}
      setSupportMultipleWindows={false}
      onError={() => {
        webViewRef.current.goBack();
      }}
/>

(I already had the token because on my app I get it on the previous step/screen, you can also render the WebView conditionally after getting the link token on the same screen).

Hope it helps!

apiel51 commented 2 years ago

^^similar to the above comment, I was able to get OAuth working as well!

One snag that held me up for a little while was that I was using encodeURI to encode the receivedRedirectUri parameter when instead I should have been using encodeURIComponent. This resulted in me getting a 400 when I would open the plaid link URL when passing that parameter since the oauth_state_id couldn't be parsed correctly.

BFMarks commented 2 years ago

Can Expo-Plaid provide more support here? Without best practices for Oauth for Expo-Plaid, the library is unusable.

JBaczuk commented 2 years ago

Happy to take PRs

BFMarks commented 2 years ago

So it's fairly easy to get the link token and the oauth_state_id but can you paste an example of your WebView component with the functions?

I have this but it's failing when triggered:

const uri_string = 'https://cdn.plaid.com/link/v2/stable/link.html?Webview=true&token={token}&receivedRedirectUri=https://alpha.cloudcastles.io/oauth-page?oauth_state_id={oauth_state_id}'
    console.log(uri_string)
    return (
      <View style={{ flex: 1, marginTop: StatusBar.currentHeight + 20 }}>
        <WebView
          // ref={webViewRef}
          ref={(ref) => (webviewRef = ref)}
          originWhitelist={['https://*', 'plaidlink://*']}
          source={{ uri: uri_string }}
          // useWebKit
          // injectedJavaScript={injectedJavaScript}
          // originWhitelist={['http://*', 'https://*', 'plaidlink://*']}
          // onShouldStartLoadWithRequest={navigationRedirect}
          // onNavigationStateChange={navigationRedirect}
          // setSupportMultipleWindows={false}
          // onError={() => {
          //   webViewRef.current.goBack();
          // }}
      />
      </View>
apiel51 commented 2 years ago

@BFMarks I believe you need to encode the redirect URI as I mentioned above. Also, this library is certainly not unusable - we use it in production 🙂

^^similar to the above comment, I was able to get OAuth working as well!

One snag that held me up for a little while was that I was using encodeURI to encode the receivedRedirectUri parameter when instead I should have been using encodeURIComponent. This resulted in me getting a 400 when I would open the plaid link URL when passing that parameter since the oauth_state_id couldn't be parsed correctly.

BFMarks commented 2 years ago

Fair enough @apiel51 - I have tried encodeURI with the same outcome, Plaid support said not to encode it which is why I posted this. Mind posting your reinitialization Webview and corresponding functions?

To my previous comment, Plaid support specifically recommended against using the Webview in this manner (but ejecting Expo to basic RN doesn't sound like a fun alternative option either). Any support would be appreciated 🙏

apiel51 commented 2 years ago

@BFMarks I think Plaid support was mistaken as certain characters must be encoded or the URI is invalid. I believe you should use encodeURIComponent as I mentioned since you are passing the redirect URI as a query parameter, not navigating to the URI itself.

Here's a snippet of our code which might be helpful:

  const webviewRef = useRef<WebView>(null);
  const plaidUri = `https://cdn.plaid.com/link/v2/stable/link.html?isWebview=true&token=${linkToken}`;
  const PLAID_OAUTH_PATH = 'open/plaid_oauth_redirect';

  useEffect(() => {
    // Listens for when our app is deeplinked to
    const handler: Linking.URLListener = ({ url }) => {
      const { path } = Linking.parse(url);

      if (path === PLAID_OAUTH_PATH) {
        webviewRef.current?.injectJavaScript?.(
          `window.open('${plaidUri}&receivedRedirectUri=${encodeURIComponent(
            url,
          )}')`,
        );
      }
    };

    Linking.addEventListener('url', handler);

    return () => {
      Linking.removeEventListener('url', handler);
    };
  }, [plaidUri]);

Yeah I assume Plaid would recommend against it but oh well 🤷

macsinte commented 1 year ago

So I managed to redirect back to my app, however I'm curious how y'all are handling the re-authentication when coming back? My experience goes from adding an account with Plaid, to a list of Bank Accounts, when a user is authenticated, to then re-directing to the app and authenticating the user.

Are y'all passing in a session token ? Or you're re-authenticating the user ?

macsinte commented 1 year ago

Also, when you re-direct back to the app, how to you pass the account information obtained in the webview? I'm confused on that

macsinte commented 1 year ago

Nevermind! thanks everyone for your help, figured everything out based on the comments above. It took a little bit to understand everything that's needed, but it totally makes sense now. Thanks!