Creating Dash Component from existing npm react package

I am trying my very hardest to create a Dash Component without any knowledge of React. I’ve named my component LoginForm and its intent is to be a Dash component implementation of the react-native-plaid-link package on npm. I am currently getting the following error on npm run start:

ERROR in ./node_modules/react-native-plaid-link/index.js 30:6
Module parse failed: Unexpected token (30:6)
You may need an appropriate loader to handle this file type.
| 
|     return (
>       <WebView
|         {...omit(this.props, [
|           'publicKey',
 @ ./src/lib/components/LoginForm.react.js 17:28-62
 @ ./src/lib/index.js
 @ ./src/demo/App.js
 @ ./src/demo/index.js
 @ multi ./src/demo/index.js
ℹ 「wdm」: Failed to compile.

I’ve read that this may have something to do with the webpack.config.js?

Below is my code:

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import PlaidAuthenticator from 'react-native-plaid-link'

export default class LoginForm extends Component {
    render() {
        const {id, data, environment, public_key, products} = this.props;

        return (
            <div id={id}>
                <PlaidAuthenticator
                    onMessage={this.onMessage}
                    publicKey={public_key}
                    env={environment}
                    product={products}
                    clientName="Butters"
                    selectAccount={false}
                />

                onMessage = (data) => {
                this.setState({data})
                }
            </div>
        );
    }
}

LoginForm.defaultProps = {};

LoginForm.propTypes = {
    /**
     * The ID used to identify this component in Dash callbacks
     */
    id: PropTypes.string,

    /**
     * The ID used to identify this component in Dash callbacks
     */
    data: PropTypes.string,

    /**
     * Dash-assigned callback that should be called whenever any of the
     * properties change
     */
    setProps: PropTypes.func,

    /**
     *
     */
    environment: PropTypes.string.isRequired,

    /**
     *
     */
    public_key: PropTypes.string.isRequired,

    /**
     *
     */
    products: PropTypes.string
};

And for convenience, the react-native-plaid-link index.js:

import React, { Component } from 'react';
import { WebView } from 'react-native';
import { PropTypes } from 'prop-types';
import omit from 'object.omit';

class PlaidAuthenticator extends Component {
  render() {
    const {
      publicKey,
      selectAccount,
      env,
      product,
      clientName,
      webhook,
      style,
      token
    } = this.props;

    let uri = `https://cdn.plaid.com/link/v2/stable/link.html?key=${
      publicKey
    }&apiVersion=v2&env=${env}&product=${product}&clientName=${
      clientName
    }&isWebView=true&isMobile=true&selectAccount=${
      selectAccount
    }`;
    uri = token !== undefined ? `${uri}&token=${token}` : uri;
    uri = webhook !== undefined ? `${uri}&webhook=${webhook}` : uri;

    return (
      <WebView
        {...omit(this.props, [
          'publicKey',
          'selectAccount',
          'env',
          'product',
          'clientName',
          'webhook',
          'token',
          'ref'
        ])}
        ref={this.props.plaidRef}
        source={{ uri }}
        onMessage={this.onMessage}
      />
    );
  }

  onMessage = e => {
    /*
      Response example for success
      {
        "action": "plaid_link-undefined::connected",
        "metadata": {
          "account": {
            "id": null,
            "name": null
          },
          "account_id": null,
          "public_token": "public-sandbox-e697e666-9ac2-4538-b152-7e56a4e59365",
          "institution": {
            "name": "Chase",
            "institution_id": "ins_3"
          }
        }
      }
    */

    this.props.onMessage(JSON.parse(e.nativeEvent.data));
  };
}

PlaidAuthenticator.propTypes = {
  publicKey: PropTypes.string.isRequired,
  onMessage: PropTypes.func.isRequired,
  env: PropTypes.string.isRequired,
  product: PropTypes.string.isRequired,
  clientName: PropTypes.string,
  webhook: PropTypes.string,
  plaidRef: PropTypes.func
};

PlaidAuthenticator.defaultProps = {
  clientName: '',
  plaidRef: () => {}
};

export default PlaidAuthenticator;

Im sure there is some simple React caveat for at least the current error I am receiving? I appreciate all help, thanks

I’m not super knowledgable about JavaScript, but normally modules published on npm should have been transpiled to plain JavaScript. In this case it looks like the contents of node_modules/react-native-plaid-link is written in JSX. By default Babel, which transpiles JSX to JavaScript, will ignore the contents of node_modules. This stackoverflow question may have some answers, though actually in your case the amount of code in the module you’re importing is small enough that you could just copy it into your src folder and build it when you build your component.

Ahh yes thank you, that definitely explains why the console is requesting a different loader. I’m not sure what you mean though by

just copy it into your src folder and build it when you build your component

since only two files currently reside in src: App.js and index.js both of which are plain JS. Unless you mean to translate the JSX content of node_modules/react-native-plaid-link(in index.js) to JS and then insert that into the existing index.js in the src directory?

Thanks for the help

Oh sorry, I should have been a bit clearer. I just meant copy their code and include it alongside the code you’ve written for your component. If you use the dash-component-boilerplate then this will be in src/lib/components, so you could put the PlaidAuthenticator definition in something like src/lib/private/PlaidAuthenticator.js and then in your LoginForm file add something like

import PlaidAuthenticator from '../private/PlaidAuthenticator';

It just can’t go in the components/ folder otherwise the Dash component generator will try to make it into a component which you maybe don’t want?

@tcbegley Thanks!! That did the trick! On to the next error haha

1 Like

@tcbegley So for the “next” error I’m running into, I end up with another unexpected token error here:

onMessage = e => {                      
          ^

Additionally, the linter is throwing alot of warnings that don’t seem to be an issue in the original node_modules/react-native-plaid-link/index.js file. Any ideas?

If it makes it easier, the code is at https://github.com/SterlingButters/plaidash

Is the repo up to date? Looks like the original issue is still a problem there? If you push all your recent changes I’ll take a look, though I don’t know how much help I can be.

My apologies, I just pushed the revision - I should’ve checked when I shared the link.

Regardless, I appreciate another set of eyes for sure. This demo file under the react-native-plaid-link repo which I thought might be helpful:

App.js:

import React, { Component } from 'react';
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
import PlaidAuthenticator from 'react-native-plaid-link';

export default class App extends Component {
  state = {
    data: {},
    status: 'LOGIN_BUTTON'
  };

  render() {
    console.log(this.state.status)

    switch(this.state.status) {
      case 'CONNECTED':
        console.log('connected')
        return this.renderDetails()
      case 'LOGIN_BUTTON':
      case 'EXIT':
        return this.renderButton();
      default:
        return this.renderLogin();
    }
  }

  renderButton = () => {
    return <View style={styles.container}>
      <TouchableOpacity onPress={() => this.setState({status: ''})}>
        <Text style={styles.paragraph}>Login with Plaid</Text>
      </TouchableOpacity>
    </View>
  }

  onLoadStart = props => {
    console.log('onLoadStart', props);
  };

  onLoad = props => {
    console.log('onLoad', props);
  };

  onLoadEnd = props => {
    console.log('onLoadEnd', props);
  };

  renderLogin() {
    return (
      <PlaidAuthenticator
        onMessage={this.onMessage}
        publicKey="eecc6d6382543dbee6478afbc5879b"
        env="sandbox"
        product="auth,transactions"
        onLoad={this.onLoad}
        onLoadStart={this.onLoadStart}
        onLoadEnd={this.onLoadEnd}
      />
    );
  }

  renderDetails() {
    return (
      <View style={styles.container}>
        <Text style={styles.paragraph}>Institution</Text>
        <Text style={styles.value}>
          {this.state.data.metadata.institution.name}
        </Text>
        <Text style={styles.paragraph}>Institution ID</Text>
        <Text style={styles.value}>
          {this.state.data.metadata.institution.institution_id}
        </Text>
        <Text style={styles.paragraph}>Token</Text>
        <Text style={styles.value}>
          {this.state.data.metadata.public_token}
        </Text>
      </View>
    );
  }

  onMessage = data => {
    // console.log(data)
    /*
      Response example for success
      {
        "action": "plaid_link-undefined::connected",
        "metadata": {
          "account": {
            "id": null,
            "name": null
          },
          "account_id": null,
          "public_token": "public-sandbox-e697e666-9ac2-4538-b152-7e56a4e59365",
          "institution": {
            "name": "Chase",
            "institution_id": "ins_3"
          }
        }
      }
    */

    this.setState({ data, status: data.action.substr(data.action.lastIndexOf(':') + 1).toUpperCase() });
  };
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    paddingTop: 24,
    backgroundColor: '#ecf0f1'
  },
  paragraph: {
    fontSize: 18,
    marginBottom: 5,
    fontWeight: 'bold',
    textAlign: 'center',
    color: '#34495e'
  },
  value: {
    marginBottom: 20,
    textAlign: 'center'
  }
});

Sorry, should have picked up on this earlier, but the library you’re trying to use is a React-Native library (for building mobile apps) rather than a ReactJS library. Pretty sure that means it’s not going to work with Dash. I found this potential replacement if you want to build a Plaid Link component.

1 Like

Haha shows how little I know about React, I saw that the demo GIF was a mobile interface but I thought that the Webview class could render anyway in a browser. I will definitely look into that replacement later today and see if maybe I can actually figure it out now - thanks for all your help!

@tcbegley I was able to get it to work thanks to your help - render at least… I’m having problems, now, trying to figure out how I would create a Dash component from it. That is, I know what I want the “output” to be but the Dash callback requires an input and I have no idea what that would be for this react component given that the component is a complex interface from which it would be difficult to isolate what the input should be. Maybe you have a suggestion?

It probably depends a bit on exactly what you want to use the component for? What are you trying to build?

Well, in Plaid’s Python API (utilizing Flask) example, once you navigate through the Plaid link “component”, some metadata is generated:

"access_token": "access-development-f88d7d01-****-4543-9c39-191d2afb****",
POST /get_access_token HTTP/1.1  "item_id": "qevaO06pJ4cNjVbN9V69cYkYbBjdvEcJw****",
  "request_id": "9XFH2oG6rZU****"
" 200 -
}

Ultimately, Id like to retrieve the metadata, specifically the access_token. The problem is there are multiple points that could be considered “inputs” and the degree to which they influence the output is kind of obscure. Hopefully, these pictures will clarify:

The plaid-react-link npm package that you referenced previously is quite complex - so much so that it would be difficult and extremely lengthy for me to decode the logic, dissect the component, and “assign” the inputs/outputs there (though I may find that to be the only alternative).

Originally, my thought was to create a Dash html.Button whose n-clicks property would initiate the rendering of this custom component (at which point I could create a get_token function that runs “until token exists” by extracting the value of the token property in the custom LoginForm{Plaid Link} component). However, the plaid-react-link npm package doesn’t actually directly render the plaid Link; it renders a button first which is followed by the rest of the component that is illustrated in the attached pictures.

UPDATE:

So this is where the component is at:

Problem:

Currently, I have a LoginForm component that has an “on-success” handler function handleOnSuccess. This the then linked to the parent component with an onTokenUpdate property defined by a “token-update” handler function handleUpdateToken. The problem is that the setState in the handleUpdateToken function is forcing an undesired rerender.

Desired Outcome:

What I ultimately need is to update the LoginForm component property token with the value obtained on success WITHOUT performing a rerender. Is this even possible? According to https://stackoverflow.com/questions/52018025/react-update-child-component-without-rerendering-parent it would seem it is not, however, no feasible alternative for my case was suggested.

Code

##LoginForm.react.js:

import React, { Component } from 'react';
import Script from 'react-load-script';
import PropTypes from 'prop-types';


class LoginForm extends Component {
    constructor(props) {
        super(props);

        this.state = {
            linkLoaded: false,
            initializeURL: 'https://cdn.plaid.com/link/v2/stable/link-initialize.js',
        };

        this.onScriptError = this.onScriptError.bind(this);
        this.onScriptLoaded = this.onScriptLoaded.bind(this);

        this.handleLinkOnLoad = this.handleLinkOnLoad.bind(this);

        this.handleOnExit = this.handleOnExit.bind(this);
        this.handleOnEvent = this.handleOnEvent.bind(this);
        this.handleOnSuccess = this.handleOnSuccess.bind(this);

        this.renderWindow = this.renderWindow.bind(this);
    }

    onScriptError() {
        console.error('There was an issue loading the link-initialize.js script');
    }

    onScriptLoaded() {
        window.linkHandler = window.Plaid.create({
            apiVersion: this.props.apiVersion,
            clientName: this.props.clientName,
            env: this.props.env,
            key: this.props.publicKey,
            onExit: this.handleOnExit,
            onLoad: this.handleLinkOnLoad,
            onEvent: this.handleOnEvent,
            onSuccess: this.handleOnSuccess,
            product: this.props.product,
            selectAccount: this.props.selectAccount,
            token: this.props.token,
            webhook: this.props.webhook,
        });

        console.log("Script loaded");
    }

    handleLinkOnLoad() {
        console.log("loaded");
        this.setState({ linkLoaded: true });
    }
    handleOnSuccess(token, metadata) {
        console.log(token);
        console.log(metadata);
        this.props.onTokenUpdate(token);
    }
    handleOnExit(error, metadata) {
        console.log('link: user exited');
        console.log(error, metadata);
    }
    handleOnLoad() {
        console.log('link: loaded');
    }
    handleOnEvent(eventname, metadata) {
        console.log('link: user event', eventname, metadata);
    }

    renderWindow() {
        const institution = this.props.institution || null;
        if (window.linkHandler) {
            window.linkHandler.open(institution);
        }
    }

    static exit(configurationObject) {
        if (window.linkHandler) {
            window.linkHandler.exit(configurationObject);
        }
    }

    render() {
        return (
            <div id={this.props.id}>
                {this.renderWindow()}
                <Script
                    url={this.state.initializeURL}
                    onError={this.onScriptError}
                    onLoad={this.onScriptLoaded}
                />
            </div>
        );
    }
}

LoginForm.defaultProps = {
    apiVersion: 'v2',
    env: 'sandbox',
    institution: null,
    selectAccount: false,
    style: {
        padding: '6px 4px',
        outline: 'none',
        background: '#FFFFFF',
        border: '2px solid #F1F1F1',
        borderRadius: '4px',
    },
};

LoginForm.propTypes = {
    // id
    id: PropTypes.string,

    // ApiVersion flag to use new version of Plaid API
    apiVersion: PropTypes.string,

    // Displayed once a user has successfully linked their account
    clientName: PropTypes.string.isRequired,

    // The Plaid API environment on which to create user accounts.
    // For development and testing, use tartan. For production, use production
    env: PropTypes.oneOf(['tartan', 'sandbox', 'development', 'production']).isRequired,

    // Open link to a specific institution, for a more custom solution
    institution: PropTypes.string,

    // The public_key associated with your account; available from
    // the Plaid dashboard (https://dashboard.plaid.com)
    publicKey: PropTypes.string.isRequired,

    // The Plaid products you wish to use, an array containing some of connect,
    // auth, identity, income, transactions, assets
    product: PropTypes.arrayOf(
        PropTypes.oneOf([
            // legacy product names
            'connect',
            'info',
            // normal product names
            'auth',
            'identity',
            'income',
            'transactions',
            'assets',
        ])
    ).isRequired,

    // Specify an existing user's public token to launch Link in update mode.
    // This will cause Link to open directly to the authentication step for
    // that user's institution.
    token: PropTypes.string,
    access_token: PropTypes.string,

    // Set to true to launch Link with the 'Select Account' pane enabled.
    // Allows users to select an individual account once they've authenticated
    selectAccount: PropTypes.bool,

    // Specify a webhook to associate with a user.
    webhook: PropTypes.string,

    // A function that is called when a user has successfully onboarded their
    // account. The function should expect two arguments, the public_key and a
    // metadata object
    onSuccess: PropTypes.func,

    // A function that is called when a user has specifically exited Link flow
    onExit: PropTypes.func,

    // A function that is called when the Link module has finished loading.
    // Calls to plaidLinkHandler.open() prior to the onLoad callback will be
    // delayed until the module is fully loaded.
    onLoad: PropTypes.func,

    // A function that is called during a user's flow in Link.
    // See
    onEvent: PropTypes.func,


    onTokenUpdate: PropTypes.func,

    // Button Styles as an Object
    style: PropTypes.object,

    // Button Class names as a String
    className: PropTypes.string,
};

export default LoginForm;

##App.js:

// /* eslint no-magic-numbers: 0 */
import React, { Component } from 'react';
import { LoginForm } from '../lib';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            access_token: null
        };
        this.handleUpdateToken = this.handleUpdateToken.bind(this)
    }

    handleUpdateToken(access_token) {
        this.setState({ access_token: access_token });
    }

    render() {
        return (
            <LoginForm
                id="Test"
                clientName="Plaid Client"
                env="sandbox"
                product={['auth', 'transactions']}
                publicKey="7a3daf1db208b7d1fe65850572eeb1"
                className="some-class-name"
                apiVersion="v2"
                onTokenUpdate={this.handleUpdateToken}
                token={this.state.access_token}
            >
            </LoginForm>
        );
    }
}

export default App;

Thanks in advance for any/all help!