Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

- Correct Layer submission types so phone number and date of birth can be
submitted separately.
- Add stable Layer and Identity Match events.
- Add a complete Layer React example and integration guidance.
- Add explicit `usePlaidLink` return types and update callback metadata types
to match the Link Web SDK.

## 4.2.0

- Add `cspNonce` to support nonce-based Content Security Policies on `usePlaidLink` and `PlaidEmbeddedLink` (partially fixes [#118](https://github.com/plaid/react-plaid-link/issues/118)).
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ a `link_token` asynchronously.
- [examples/hooks.tsx](examples/hooks.tsx): example using hooks with all
available callbacks
- [examples/oauth.tsx](examples/oauth.tsx): example handling OAuth with hooks
- [examples/layer.tsx](examples/layer.tsx): example implementing Plaid Layer

```tsx
import React from 'react';
Expand Down Expand Up @@ -81,13 +82,16 @@ the various Link options and the
| key | type |
| --------------------- | ----------------------------------------------------------------------------------------- |
| `token` | `string \| null` |
| `onSuccess` | `(public_token: string, metadata: PlaidLinkOnSuccessMetadata) => void` |
| `onSuccess` | `(public_token: string \| null, metadata: PlaidLinkOnSuccessMetadata) => void` |
| `onExit` | `(error: null \| PlaidLinkError, metadata: PlaidLinkOnExitMetadata) => void` |
| `onEvent` | `(eventName: PlaidLinkStableEvent \| string, metadata: PlaidLinkOnEventMetadata) => void` |
| `onLoad` | `() => void` |
| `receivedRedirectUri` | `string \| undefined` |
| `cspNonce` | `string \| undefined` |

`public_token` is `null` for flows such as Identity Verification that do not
create an Item.

#### Content Security Policy nonce

If your app uses a nonce-based Content Security Policy, generate a fresh nonce
Expand Down Expand Up @@ -132,6 +136,10 @@ const { open, ready } = usePlaidLink({
| `error` | `ErrorEvent \| null` |
| `exit` | `(options?: { force?: boolean }, callback?: () => void) => void` |

For Layer, call `submit` with either `phone_number` or `date_of_birth`. See the
[complete Layer example](examples/layer.tsx) and
[Plaid Layer integration guide](https://plaid.com/docs/layer/add-to-app/).

### Handling an invalid Link token

If `onExit` receives an `INVALID_LINK_TOKEN` error, fetch a new Link token and
Expand Down
105 changes: 105 additions & 0 deletions examples/layer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from 'react';
import {
PlaidLinkOnEvent,
PlaidLinkOnSuccess,
PlaidLinkStableEvent,
usePlaidLink,
} from 'react-plaid-link';

interface LayerExampleProps {
linkToken: string;
onSuccess: PlaidLinkOnSuccess;
onFallback: () => void;
}

export const LayerExample: React.FC<LayerExampleProps> = ({
linkToken,
onSuccess,
onFallback,
}) => {
const [phoneNumber, setPhoneNumber] = React.useState('');
const [dateOfBirth, setDateOfBirth] = React.useState('');
const [
layerEvent,
setLayerEvent,
] = React.useState<PlaidLinkStableEvent | null>(null);

const onEvent = React.useCallback<PlaidLinkOnEvent>(eventName => {
switch (eventName) {
case PlaidLinkStableEvent.LAYER_READY:
setLayerEvent(PlaidLinkStableEvent.LAYER_READY);
break;
case PlaidLinkStableEvent.LAYER_NOT_AVAILABLE:
setLayerEvent(PlaidLinkStableEvent.LAYER_NOT_AVAILABLE);
break;
case PlaidLinkStableEvent.LAYER_AUTOFILL_NOT_AVAILABLE:
setLayerEvent(PlaidLinkStableEvent.LAYER_AUTOFILL_NOT_AVAILABLE);
break;
default:
break;
}
}, []);

// Initialize Link as soon as the view mounts so Layer can preload.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

const { open, ready, submit } = usePlaidLink({
token: linkToken,
onSuccess,
onEvent,
});

React.useEffect(() => {
if (ready && layerEvent === PlaidLinkStableEvent.LAYER_READY) {
open();
}
}, [layerEvent, open, ready]);

React.useEffect(() => {
if (layerEvent === PlaidLinkStableEvent.LAYER_AUTOFILL_NOT_AVAILABLE) {
onFallback();
}
}, [layerEvent, onFallback]);

const submitPhoneNumber = (event: React.FormEvent) => {
event.preventDefault();
submit({ phone_number: phoneNumber });
};

const submitDateOfBirth = (event: React.FormEvent) => {
event.preventDefault();
submit({ date_of_birth: dateOfBirth });
};

if (layerEvent === PlaidLinkStableEvent.LAYER_NOT_AVAILABLE) {
return (
<form onSubmit={submitDateOfBirth}>
<label>
Date of birth
<input
type="date"
value={dateOfBirth}
onChange={event => setDateOfBirth(event.target.value)}
/>
</label>
<button type="submit" disabled={!ready || !dateOfBirth}>
Continue
</button>
</form>
);
}

return (
<form onSubmit={submitPhoneNumber}>
<label>
Phone number
<input
type="tel"
value={phoneNumber}
onChange={event => setPhoneNumber(event.target.value)}
/>
</label>
<button type="submit" disabled={!ready || !phoneNumber}>
Continue
</button>
</form>
);
};
25 changes: 20 additions & 5 deletions src/PlaidEmbeddedLink.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import { PlaidEmbeddedLink, PlaidLinkOptions } from './';
import { PlaidEmbeddedHandler, PlaidEmbeddedLink, PlaidLinkOptions } from './';

import useScript from './react-script-hook';
jest.mock('./react-script-hook');
Expand All @@ -20,13 +20,17 @@ describe('PlaidEmbeddedLink', () => {
onLoad: jest.fn(),
onEvent: jest.fn(),
};
const createEmbeddedSpy = jest.fn(() => ({
const createEmbeddedSpy = jest.fn<
PlaidEmbeddedHandler,
[PlaidLinkOptions, HTMLElement]
>(() => ({
destroy: jest.fn(),
}));

beforeEach(() => {
mockedUseScript.mockImplementation(() => ScriptLoadingState.LOADED);
window.Plaid = {
create: jest.fn(),
createEmbedded: createEmbeddedSpy,
};
});
Expand All @@ -36,10 +40,21 @@ describe('PlaidEmbeddedLink', () => {
});

it('should not rerender if config did not change', () => {
const styles = { height: '350px', width: '350px', backgroundColor: 'white' };
const { rerender } = render(<PlaidEmbeddedLink {...config} style={styles} />);
const styles = {
height: '350px',
width: '350px',
backgroundColor: 'white',
};
const { rerender } = render(
<PlaidEmbeddedLink {...config} style={styles} />
);
expect(createEmbeddedSpy).toHaveBeenCalledTimes(1);
rerender(<PlaidEmbeddedLink {...config} style={{...styles, backgroundColor: 'light-blue'}} />);
rerender(
<PlaidEmbeddedLink
{...config}
style={{ ...styles, backgroundColor: 'light-blue' }}
/>
);
expect(createEmbeddedSpy).toHaveBeenCalledTimes(1);
});

Expand Down
27 changes: 17 additions & 10 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ import {
PlaidLinkOnSuccess,
PlaidLinkOnSuccessMetadata,
CommonPlaidLinkOptions,
PlaidHandlerExitOptions,
} from './types';

export interface PlaidFactory {
open: (() => void) | Function;
submit: ((data: PlaidHandlerSubmissionData) => void)| Function;
exit: ((exitOptions: any, callback: () => void) => void) | Function;
destroy: (() => void) | Function;
open: () => void;
submit: (data: PlaidHandlerSubmissionData) => void;
exit: (exitOptions?: PlaidHandlerExitOptions, callback?: () => void) => void;
destroy: () => void;
}

interface FactoryInternalState {
plaid: PlaidHandler | null;
open: boolean;
onExitCallback: (() => void) | null | Function;
onExitCallback: (() => void) | null;
}

const renameKeyInObject = (
Expand Down Expand Up @@ -52,7 +53,10 @@ const createPlaidHandler = <

state.plaid = creator({
...config,
onSuccess: (publicToken: string, metadata: PlaidLinkOnSuccessMetadata) => {
onSuccess: (
publicToken: string | null,
metadata: PlaidLinkOnSuccessMetadata
) => {
state.open = false;
config.onSuccess(publicToken, metadata);
},
Expand All @@ -76,15 +80,18 @@ const createPlaidHandler = <
if (!state.plaid) {
return;
}
state.plaid.submit(data)
}
state.plaid.submit(data);
};

const exit = (exitOptions: any, callback: (() => void) | Function) => {
const exit = (
exitOptions?: PlaidHandlerExitOptions,
callback?: () => void
) => {
if (!state.open || !state.plaid) {
callback && callback();
return;
}
state.onExitCallback = callback;
state.onExitCallback = callback || null;
state.plaid.exit(exitOptions);
if (exitOptions && exitOptions.force) {
state.open = false;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { usePlaidLink } from './usePlaidLink';
export { PlaidLink } from './PlaidLink';
export { PlaidEmbeddedLink } from './PlaidEmbeddedLink';
export * from './types'
export * from './types';
51 changes: 40 additions & 11 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ export interface PlaidLinkError {
error_type: string;
error_code: string;
error_message: string;
display_message: string;
display_message: string | null;
}

export interface PlaidLinkOnSuccessMetadata {
institution: null | PlaidInstitution;
accounts: Array<PlaidAccount>;
link_session_id: string;
transfer_status?: string;
transfer_status?: string | null;
}

export interface PlaidLinkOnExitMetadata {
Expand All @@ -38,14 +38,18 @@ export interface PlaidLinkOnExitMetadata {
}

export interface PlaidLinkOnEventMetadata {
account_number_mask: null | string;
error_type: null | string;
error_code: null | string;
error_message: null | string;
exit_status: null | string;
institution_id: null | string;
institution_name: null | string;
institution_search_query: null | string;
is_update_mode: null | string;
match_reason: null | string;
mfa_type: null | string;
routing_number: null | string;
// see possible values for view_name at https://plaid.com/docs/link/web/#link-web-onevent-view-name
view_name: null | string;
// see possible values for selection at https://plaid.com/docs/link/web/#link-web-onevent-selection
Expand All @@ -57,7 +61,7 @@ export interface PlaidLinkOnEventMetadata {
}

export type PlaidLinkOnSuccess = (
public_token: string,
public_token: string | null,
metadata: PlaidLinkOnSuccessMetadata
) => void;

Expand All @@ -74,8 +78,13 @@ export enum PlaidLinkStableEvent {
SELECT_INSTITUTION = 'SELECT_INSTITUTION',
ERROR = 'ERROR',
BANK_INCOME_INSIGHTS_COMPLETED = 'BANK_INCOME_INSIGHTS_COMPLETED',
IDENTITY_MATCH_FAILED = 'IDENTITY_MATCH_FAILED',
IDENTITY_MATCH_PASSED = 'IDENTITY_MATCH_PASSED',
IDENTITY_VERIFICATION_PASS_SESSION = 'IDENTITY_VERIFICATION_PASS_SESSION',
IDENTITY_VERIFICATION_FAIL_SESSION = 'IDENTITY_VERIFICATION_FAIL_SESSION'
IDENTITY_VERIFICATION_FAIL_SESSION = 'IDENTITY_VERIFICATION_FAIL_SESSION',
LAYER_READY = 'LAYER_READY',
LAYER_NOT_AVAILABLE = 'LAYER_NOT_AVAILABLE',
LAYER_AUTOFILL_NOT_AVAILABLE = 'LAYER_AUTOFILL_NOT_AVAILABLE',
}

export type PlaidLinkOnEvent = (
Expand All @@ -90,7 +99,7 @@ export type PlaidLinkOnEvent = (
export type PlaidLinkOnLoad = () => void;

export interface CommonPlaidLinkOptions<T> {
// A function that is called when a user has successfully connected an Item.
// A function that is called when a user has successfully completed Link.
// The function should expect two arguments, the public_token and a metadata object
onSuccess: T;
// A callback that is called when a user has specifically exited Link flow
Expand Down Expand Up @@ -174,25 +183,45 @@ export type PlaidEmbeddedLinkPropTypes = PlaidLinkOptionsWithLinkToken & {
style?: React.CSSProperties;
};

export type PlaidHandlerSubmissionData = {
phone_number: string | null;
date_of_birth: string | null;
export type PlaidHandlerSubmissionData =
| {
phone_number: string;
date_of_birth?: never;
}
| {
phone_number?: never;
date_of_birth: string;
};

export interface PlaidHandlerExitOptions {
force?: boolean;
}

export interface PlaidHandler {
open: () => void;
submit: (data: PlaidHandlerSubmissionData) => void;
exit: (force?: boolean) => void;
exit: (options?: PlaidHandlerExitOptions) => void;
destroy: () => void;
}

export interface PlaidLinkResult {
error: ErrorEvent | null;
ready: boolean;
submit: (data: PlaidHandlerSubmissionData) => void;
exit: (options?: PlaidHandlerExitOptions, callback?: () => void) => void;
open: () => void;
}

export interface PlaidEmbeddedHandler {
destroy: () => void;
}

export interface Plaid extends PlaidHandler {
export interface Plaid {
create: (config: PlaidLinkOptions) => PlaidHandler;
createEmbedded: (config: PlaidLinkOptions, domTarget: HTMLElement) => PlaidEmbeddedHandler;
createEmbedded: (
config: PlaidLinkOptions,
domTarget: HTMLElement
) => PlaidEmbeddedHandler;
}

declare global {
Expand Down
Loading
Loading