UNPKG

react-plaid-link

Version:
558 lines (454 loc) 17.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) : typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) : (global = global || self, factory(global.PlaidLink = {}, global.React)); }(this, (function (exports, React) { 'use strict'; var React__default = 'default' in React ? React['default'] : React; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var _excluded = ["src", "checkForExisting"]; var scripts = {}; // Check for existing <script> tags with this src. If so, update scripts[src] // and return the new status; otherwise, return undefined. var checkExisting = function checkExisting(src) { var existing = document.querySelector("script[src=\"".concat(src, "\"]")); if (existing) { // Assume existing <script> tag is already loaded, // and cache that data for future use. return scripts[src] = { loading: false, error: null, scriptEl: existing }; } return undefined; }; var isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; function useScript(_ref) { var src = _ref.src, _ref$checkForExisting = _ref.checkForExisting, checkForExisting = _ref$checkForExisting === void 0 ? false : _ref$checkForExisting, attributes = _objectWithoutProperties(_ref, _excluded); // Check whether some instance of this hook considered this src. var status = src ? scripts[src] : undefined; // If requested, check for existing <script> tags with this src // (unless we've already loaded the script ourselves). if (!status && checkForExisting && src && isBrowser) { status = checkExisting(src); } var _useState = React.useState(status ? status.loading : Boolean(src)), _useState2 = _slicedToArray(_useState, 2), loading = _useState2[0], setLoading = _useState2[1]; var _useState3 = React.useState(status ? status.error : null), _useState4 = _slicedToArray(_useState3, 2), error = _useState4[0], setError = _useState4[1]; // Tracks if script is loaded so we can avoid duplicate script tags var _useState5 = React.useState(false), _useState6 = _slicedToArray(_useState5, 2), scriptLoaded = _useState6[0], setScriptLoaded = _useState6[1]; React.useEffect(function () { // Nothing to do on server, or if no src specified, or // if script is already loaded or "error" state. if (!isBrowser || !src || scriptLoaded || error) return; // Check again for existing <script> tags with this src // in case it's changed since mount. // eslint-disable-next-line react-hooks/exhaustive-deps status = scripts[src]; if (!status && checkForExisting) { status = checkExisting(src); } // Determine or create <script> element to listen to. var scriptEl; if (status) { scriptEl = status.scriptEl; } else { scriptEl = document.createElement('script'); scriptEl.src = src; Object.keys(attributes).forEach(function (key) { if (scriptEl[key] === undefined) { scriptEl.setAttribute(key, attributes[key]); } else { scriptEl[key] = attributes[key]; } }); status = scripts[src] = { loading: true, error: null, scriptEl: scriptEl }; } // `status` is now guaranteed to be defined: either the old status // from a previous load, or a newly created one. var handleLoad = function handleLoad() { if (status) status.loading = false; setLoading(false); setScriptLoaded(true); }; var handleError = function handleError(error) { if (status) status.error = error; setError(error); }; scriptEl.addEventListener('load', handleLoad); scriptEl.addEventListener('error', handleError); document.body.appendChild(scriptEl); return function () { scriptEl.removeEventListener('load', handleLoad); scriptEl.removeEventListener('error', handleError); // if we unmount, and we are still loading the script, then // remove from the DOM & cache so we have a clean slate next time. // this is similar to the `removeOnUnmount` behavior of the TS useScript hook // https://github.com/juliencrn/usehooks-ts/blob/20667273744a22dd2cd2c48c38cd3c10f254ae47/packages/usehooks-ts/src/useScript/useScript.ts#L134 // but only applied when loading if (status && status.loading) { scriptEl.remove(); delete scripts[src]; } }; // we need to ignore the attributes as they're a new object per call, so we'd never skip an effect call }, [src]); return [loading, error]; } var renameKeyInObject = function renameKeyInObject(o, oldKey, newKey) { var newObject = {}; delete Object.assign(newObject, o, _defineProperty({}, newKey, o[oldKey]))[oldKey]; return newObject; }; /** * Wrap link handler creation and instance to clean up iframe via destroy() method */ var createPlaidHandler = function createPlaidHandler(config, creator) { var state = { plaid: null, open: false, onExitCallback: null }; // If Plaid is not available, throw an Error if (typeof window === 'undefined' || !window.Plaid) { throw new Error('Plaid not loaded'); } state.plaid = creator(_objectSpread2(_objectSpread2({}, config), {}, { onExit: function onExit(error, metadata) { state.open = false; config.onExit && config.onExit(error, metadata); state.onExitCallback && state.onExitCallback(); } })); var open = function open() { if (!state.plaid) { return; } state.open = true; state.onExitCallback = null; state.plaid.open(); }; var submit = function submit(data) { if (!state.plaid) { return; } state.plaid.submit(data); }; var exit = function exit(exitOptions, callback) { if (!state.open || !state.plaid) { callback && callback(); return; } state.onExitCallback = callback; state.plaid.exit(exitOptions); if (exitOptions && exitOptions.force) { state.open = false; } }; var destroy = function destroy() { if (!state.plaid) { return; } state.plaid.destroy(); state.plaid = null; }; return { open: open, submit: submit, exit: exit, destroy: destroy }; }; var createPlaid = function createPlaid(options, creator) { var config = renameKeyInObject(options, 'publicKey', 'key'); return createPlaidHandler(config, creator); }; var PLAID_LINK_STABLE_URL = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; var noop = function noop() {}; /** * This hook loads Plaid script and manages the Plaid Link creation for you. * You get easy open & exit methods to call and loading & error states. * * This will destroy the Plaid UI on un-mounting so it's up to you to be * graceful to the user. * * A new Plaid instance is created every time the token and products options change. * It's up to you to prevent unnecessary re-creations on re-render. */ var usePlaidLink = function usePlaidLink(options) { // Asynchronously load the plaid/link/stable url into the DOM var _useScript = useScript({ src: PLAID_LINK_STABLE_URL, checkForExisting: true }), _useScript2 = _slicedToArray(_useScript, 2), loading = _useScript2[0], error = _useScript2[1]; // internal state var _useState = React.useState(null), _useState2 = _slicedToArray(_useState, 2), plaid = _useState2[0], setPlaid = _useState2[1]; var _useState3 = React.useState(false), _useState4 = _slicedToArray(_useState3, 2), iframeLoaded = _useState4[0], setIframeLoaded = _useState4[1]; var products = (options.product || []).slice().sort().join(','); React.useEffect(function () { // If the link.js script is still loading, return prematurely if (loading) { return; } // If the token, publicKey, and received redirect URI are undefined, return prematurely if (!options.token && !options.publicKey && !options.receivedRedirectUri) { return; } if (error || !window.Plaid) { // eslint-disable-next-line no-console console.error('Error loading Plaid', error); return; } // if an old plaid instance exists, destroy it before // creating a new one if (plaid != null) { plaid.exit({ force: true }, function () { return plaid.destroy(); }); } var next = createPlaid(_objectSpread2(_objectSpread2({}, options), {}, { onLoad: function onLoad() { setIframeLoaded(true); options.onLoad && options.onLoad(); } }), window.Plaid.create); setPlaid(next); // destroy the Plaid iframe factory return function () { return next.exit({ force: true }, function () { return next.destroy(); }); }; }, [loading, error, options.publicKey, options.token, products]); var ready = plaid != null && (!loading || iframeLoaded); var openNoOp = function openNoOp() { if (!options.token) { console.warn('react-plaid-link: You cannot call open() without a valid token supplied to usePlaidLink. This is a no-op.'); } }; return { error: error, ready: ready, submit: plaid ? plaid.submit : noop, exit: plaid ? plaid.exit : noop, open: plaid ? plaid.open : openNoOp }; }; var _excluded$1 = ["children", "style", "className"]; var PlaidLink = function PlaidLink(props) { var children = props.children, style = props.style, className = props.className, config = _objectWithoutProperties(props, _excluded$1); var _usePlaidLink = usePlaidLink(_objectSpread2({}, config)), error = _usePlaidLink.error, open = _usePlaidLink.open; return /*#__PURE__*/React__default.createElement("button", { disabled: Boolean(error), type: "button", className: className, style: _objectSpread2({ padding: '6px 4px', outline: 'none', background: '#FFFFFF', border: '2px solid #F1F1F1', borderRadius: '4px' }, style), onClick: function onClick() { return open(); } }, children); }; PlaidLink.displayName = 'PlaidLink'; var PlaidEmbeddedLink = function PlaidEmbeddedLink(props) { var style = props.style, className = props.className, onSuccess = props.onSuccess, onExit = props.onExit, onLoad = props.onLoad, onEvent = props.onEvent, token = props.token, receivedRedirectUri = props.receivedRedirectUri; var config = React.useMemo(function () { return { onSuccess: onSuccess, onExit: onExit, onLoad: onLoad, onEvent: onEvent, token: token, receivedRedirectUri: receivedRedirectUri }; }, [onSuccess, onExit, onLoad, onEvent, token, receivedRedirectUri]); // Asynchronously load the plaid/link/stable url into the DOM var _useScript = useScript({ src: PLAID_LINK_STABLE_URL, checkForExisting: true }), _useScript2 = _slicedToArray(_useScript, 2), loading = _useScript2[0], error = _useScript2[1]; var embeddedLinkTarget = React.useRef(null); React.useEffect(function () { // If the external link JS script is still loading, return prematurely if (loading) { return; } if (error || !window.Plaid) { // eslint-disable-next-line no-console console.error('Error loading Plaid', error); return; } if (config.token == null || config.token == '') { console.error('A token is required to initialize embedded Plaid Link'); return; } // The embedded Link interface doesn't use the `usePlaidLink` hook to manage // its Plaid Link instance because the embedded Link integration in link-initialize // maintains its own handler internally. var _window$Plaid$createE = window.Plaid.createEmbedded(_objectSpread2({}, config), embeddedLinkTarget.current), destroy = _window$Plaid$createE.destroy; // Clean up embedded Link component on unmount return function () { destroy(); }; }, [loading, error, config, embeddedLinkTarget]); return /*#__PURE__*/React__default.createElement("div", { style: style, className: className, ref: embeddedLinkTarget }); }; // The following event names are stable and will not be deprecated or changed (function (PlaidLinkStableEvent) { PlaidLinkStableEvent["OPEN"] = "OPEN"; PlaidLinkStableEvent["EXIT"] = "EXIT"; PlaidLinkStableEvent["HANDOFF"] = "HANDOFF"; PlaidLinkStableEvent["SELECT_INSTITUTION"] = "SELECT_INSTITUTION"; PlaidLinkStableEvent["ERROR"] = "ERROR"; PlaidLinkStableEvent["BANK_INCOME_INSIGHTS_COMPLETED"] = "BANK_INCOME_INSIGHTS_COMPLETED"; PlaidLinkStableEvent["IDENTITY_VERIFICATION_PASS_SESSION"] = "IDENTITY_VERIFICATION_PASS_SESSION"; PlaidLinkStableEvent["IDENTITY_VERIFICATION_FAIL_SESSION"] = "IDENTITY_VERIFICATION_FAIL_SESSION"; })(exports.PlaidLinkStableEvent || (exports.PlaidLinkStableEvent = {})); exports.PlaidEmbeddedLink = PlaidEmbeddedLink; exports.PlaidLink = PlaidLink; exports.usePlaidLink = usePlaidLink; Object.defineProperty(exports, '__esModule', { value: true }); })));