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
31 changes: 31 additions & 0 deletions packages/react-embed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ const EmbedFlowExample: React.FunctionComponent = () => (
);
```

Use the separate `SecureEmbedFlow` component to pass responder data in a POST
body:

```tsx
import { SecureEmbedFlow } from '@formsort/react-embed';

<SecureEmbedFlow
clientLabel="formsort"
flowLabel="onboarding"
// responderUuid={optionalResponderUuid}
initialAnswers={{
firstName: 'Olivia',
}}
/>;
```

`responderUuid` and `initialAnswers` are optional. `SecureEmbedFlow` does not
accept `queryParams` or `formsortEnv`. It has the same configuration and event
props as `EmbedFlow`.

### Events

You can add event listeners to flows like `Flowloaded`, `redirect` etc. See [all event listeners](https://github.com/formsort/oss/tree/master/packages/web-embed-api#event-listeners)
Expand All @@ -58,6 +78,17 @@ You can add event listeners to flows like `Flowloaded`, `redirect` etc. See [all
| onRedirect | [event listener](https://github.com/formsort/oss/tree/master/packages/web-embed-api#redirect--url-string-answers--key-string-any-----cancel-boolean---undefined) | no | `(url: string) => { console.log('redirecting to:', url) }` |
| onUnauthorized | [event listener](https://github.com/formsort/oss/tree/master/packages/web-embed-api#unauthorized---void) | no | `() => { console.log('ID token is missing or invalid.') }` |

### SecureEmbedFlow props

`SecureEmbedFlow` supports `clientLabel`, `flowLabel`, `variantLabel`,
`embedConfig`, and all event props from `EmbedFlow`. It also supports these
secure POST props:

| Prop name | Description | Required | Example values |
| -------------- | ------------------------------------- | -------- | -------------------------------------- |
| responderUuid | responder UUID sent in the POST body | no | `e4923baa-dc2d-4555-813c-a166952292fa` |
| initialAnswers | initial answers sent in the POST body | no | `{ firstName: 'Olivia' }` |

### Loading a specific variant revision

You can use query parameters to load a specific variant revision. Don't use it if you want to show latest variant.
Expand Down
51 changes: 48 additions & 3 deletions packages/react-embed/src/__tests__/EmbedFlow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import FormsortWebEmbed, { IFormsortWebEmbed } from '@formsort/web-embed-api';
import FormsortWebEmbed, {
FormsortSecureWebEmbed,
IFormsortSecureWebEmbed,
IFormsortWebEmbed,
} from '@formsort/web-embed-api';
import { render } from '@testing-library/react';
import React from 'react';

import EmbedFlow from '..';
import EmbedFlow, { SecureEmbedFlow } from '..';

jest.mock('@formsort/web-embed-api');

const mockWebEmbedApi = FormsortWebEmbed as jest.MockedFunction<
typeof FormsortWebEmbed
>;
const mockSecureWebEmbedApi = FormsortSecureWebEmbed as jest.MockedFunction<
typeof FormsortSecureWebEmbed
>;

describe('EmbedFlow component', () => {
let loadMock: jest.Mock;
let embedMock: IFormsortWebEmbed;
let secureEmbedMock: IFormsortSecureWebEmbed;
let addEventListenerMock: jest.Mock;
let removeEventListenerMock: jest.Mock;

Expand All @@ -27,10 +35,16 @@ describe('EmbedFlow component', () => {
addEventListener: addEventListenerMock,
removeEventListener: removeEventListenerMock,
};
secureEmbedMock = {
...embedMock,
loadFlow: loadMock,
};
mockWebEmbedApi.mockReturnValueOnce(embedMock);
mockSecureWebEmbedApi.mockReturnValue(secureEmbedMock);
});
afterEach(() => {
mockWebEmbedApi.mockClear();
mockSecureWebEmbedApi.mockClear();
});

test('should load flows without variant label', () => {
Expand Down Expand Up @@ -105,7 +119,7 @@ describe('EmbedFlow component', () => {
);
});

test('should load flows with URL params', () => {
test('should load flows with URL parameters', () => {
const uuid = 'b1c7d9c8-f4b0-4f3f-9fc3-abf32ae8a061';
render(
<EmbedFlow
Expand All @@ -114,16 +128,47 @@ describe('EmbedFlow component', () => {
variantLabel="test-variant"
responderUuid={uuid}
formsortEnv="staging"
queryParams={[['campaign', 'summer']]}
/>
);
expect(loadMock).toBeCalledWith(
'test-client',
'test-flow',
'test-variant',
[
['campaign', 'summer'],
['responderUuid', uuid],
['formsortEnv', 'staging'],
]
);
});

test('should load sensitive data with the secure web embed', () => {
const responderUuid = 'b1c7d9c8-f4b0-4f3f-9fc3-abf32ae8a061';
const initialAnswers = {
firstName: 'Olivia',
};

render(
<SecureEmbedFlow
flowLabel="test-flow"
clientLabel="test-client"
responderUuid={responderUuid}
initialAnswers={initialAnswers}
/>
);

expect(mockSecureWebEmbedApi).toHaveBeenCalledWith(
expect.any(HTMLDivElement),
undefined
);
expect(mockWebEmbedApi).not.toHaveBeenCalled();
expect(loadMock).toHaveBeenCalledWith(
'test-client',
'test-flow',
undefined,
responderUuid,
initialAnswers
);
});
});
70 changes: 68 additions & 2 deletions packages/react-embed/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { SupportedAnalyticsEvent } from '@formsort/embed-messaging-manager';
import FormsortWebEmbed, {
FormsortInitialAnswers,
FormsortSecureWebEmbed,
IEventMap,
IFormsortSecureWebEmbed,
IFormsortWebEmbed,
IFormsortWebEmbedConfig,
} from '@formsort/web-embed-api';
Expand Down Expand Up @@ -34,6 +37,14 @@ export interface IReactEmbedEventMap {

export type EmbedFlowProps = ILoadProps & IReactEmbedEventMap;

export type SecureEmbedFlowProps = Pick<
ILoadProps,
'clientLabel' | 'flowLabel' | 'variantLabel' | 'embedConfig'
> & {
responderUuid?: string;
initialAnswers?: FormsortInitialAnswers;
} & IReactEmbedEventMap;

export const eventMapping: Record<keyof IReactEmbedEventMap, keyof IEventMap> =
{
onUnauthorized: 'unauthorized',
Expand All @@ -46,7 +57,7 @@ export const eventMapping: Record<keyof IReactEmbedEventMap, keyof IEventMap> =
};

const attachEventListenersToEmbed = (
embed: IFormsortWebEmbed,
embed: IFormsortWebEmbed | IFormsortSecureWebEmbed,
events: IReactEmbedEventMap
): void => {
for (const [reactEventName, listener] of Object.entries(events)) {
Expand Down Expand Up @@ -78,7 +89,6 @@ const onMount = (

const embed = FormsortWebEmbed(containerElement, embedConfig);
attachEventListenersToEmbed(embed, eventListeners);

if (responderUuid) {
queryParams.push(['responderUuid', responderUuid]);
}
Expand All @@ -96,6 +106,36 @@ const onMount = (
return embed;
};

const onSecureMount = (
containerRef: React.RefObject<HTMLDivElement>,
props: SecureEmbedFlowProps
): IFormsortSecureWebEmbed | undefined => {
const containerElement = containerRef.current;
if (!containerElement) {
return;
}

const {
clientLabel,
flowLabel,
variantLabel,
embedConfig,
responderUuid,
initialAnswers,
...eventListeners
} = props;
const embed = FormsortSecureWebEmbed(containerElement, embedConfig);
attachEventListenersToEmbed(embed, eventListeners);
embed.loadFlow(
clientLabel,
flowLabel,
variantLabel,
responderUuid,
initialAnswers
);
return embed;
};

const EmbedFlow: React.FunctionComponent<EmbedFlowProps> = (props) => {
const containerRef = useRef<HTMLDivElement>(null);
const style = props.embedConfig?.style;
Expand All @@ -120,4 +160,30 @@ const EmbedFlow: React.FunctionComponent<EmbedFlowProps> = (props) => {
return <div ref={containerRef} style={style} />;
};

export const SecureEmbedFlow: React.FunctionComponent<SecureEmbedFlowProps> = (
props
) => {
const containerRef = useRef<HTMLDivElement>(null);
const style = props.embedConfig?.style;
const [flowClosed, setFlowClosed] = useState(false);

useEffect(() => {
const embed = onSecureMount(containerRef, props);

embed?.addEventListener(SupportedAnalyticsEvent.FlowClosed, () => {
setFlowClosed(true);
});

return () => {
embed?.unloadFlow();
};
}, []);

if (flowClosed) {
return null;
}

return <div ref={containerRef} style={style} />;
};

export default EmbedFlow;
39 changes: 39 additions & 0 deletions packages/web-embed-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,45 @@ Initializes a Formsort iframe as a child of the `rootEl` provided.
const embed = FormsortWebEmbed(document.body);
```

Use `FormsortSecureWebEmbed` to pass responder data in a POST body instead of
the URL. Calling `loadFlow` submits the POST navigation to the iframe.

```ts
import { FormsortSecureWebEmbed } from '@formsort/web-embed-api';

const embed = FormsortSecureWebEmbed(document.body);

embed.loadFlow(
'formsort',
'onboarding'
// 'main', // [optional] variantLabel
// 'e4923baa-dc2d-4555-813c-a166952292fa', // [optional] responderUuid
// { firstName: 'Olivia' } // [optional] initialAnswers
);
```

The POST data can contain scalar values, arrays, nested objects, and arrays of
objects. The secure embed uses the same configuration, methods, and events as
`FormsortWebEmbed`.

### `FormsortSecureWebEmbed(rootEl: HTMLElement, config?: IFormsortWebEmbedConfig)`

Initializes a secure Formsort iframe. It has the same configuration, methods,
and events as `FormsortWebEmbed`.

Its `loadFlow` method accepts optional `responderUuid` and `initialAnswers`
values. It does not accept query parameters:

```ts
loadFlow(
clientLabel: string,
flowLabel: string,
variantLabel?: string,
responderUuid?: string,
initialAnswers?: FormsortInitialAnswers
) => void;
```

The optional `config` object has the following interface:

```ts
Expand Down
Loading
Loading