react-plaid-link
Version:
A React component for Plaid Link
558 lines (454 loc) • 17.8 kB
JavaScript
(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 });
})));