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
35 changes: 22 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,20 @@ embed.on(MESSAGE_KIND.TOAST, (data: ToastMessageDataV1) => {

The full list of events is below. Events marked _none_ in the **Data** column are fired without a payload; events with a linked payload have their data shape documented further down.

| Event | Constant | Data | When it fires |
| ---------------------------- | ------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Toast message | `MESSAGE_KIND.TOAST` | [See payload](#toast-payload) | A notification should be shown to the user, e.g. when they have selected a fund. |
| Window dimension change | `MESSAGE_KIND.WINDOW_DIMENSION_CHANGE` | [See payload](#window-dimension-change-payload) | The DOM contents of the embed have changed and caused the height of the embed to change. |
| Loaded | `MESSAGE_KIND.LOADED` | _none_ | The embed has finished loading. |
| Employer settings updated¹ | `MESSAGE_KIND.EMPLOYER_SETTINGS_UPDATED` | _none_ | An update has been made to the employer settings but the change has not yet been delivered to the partner system. |
| Employer settings committed¹ | `MESSAGE_KIND.EMPLOYER_SETTINGS_COMMITTED` | _none_ | A change to the employer settings has been committed into the partner system (the partner webhook responded with a `<400` status code). |
| Onboarding intent completed | `MESSAGE_KIND.ONBOARDING_INTENT_COMPLETED` | _none_ | A user has completed their onboarding session intent. Useful for showing a "processing" state while waiting for the webhook to complete. |
| Onboarding session committed | `MESSAGE_KIND.ONBOARDING_SESSION_COMMITTED` | _none_ | A user has completed the onboarding flow and the data has been delivered into the partner system (the same "committed" rules as employer settings apply here). |
| Onboarding session finished | `MESSAGE_KIND.ONBOARDING_SESSION_FINISHED` | _none_ | A user has finished the onboarding flow but the payload has not yet been transmitted. Use this to move the user to the next step without waiting on async provisioning. |
| Onboarding step changed | `MESSAGE_KIND.ONBOARDING_STEP_CHANGED` | [See payload](#onboarding-step-changed-payload) | The embed has loaded, or the user has moved to the next step in the onboarding flow. |
| MFA verification completed | `MESSAGE_KIND.MFA_VERIFICATION_COMPLETED` | [See payload](#mfa-verification-completed-payload) | The user has finished their MFA session, either by verifying successfully or by exhausting the maximum number of verification attempts. |
| Page loaded | `MESSAGE_KIND.PAGE_LOADED` | _none_ | A new page is loaded inside the embed. The embed will also scroll the iFrame to the top of the viewport so the new page is visible. |
| Event | Constant | Data | When it fires |
| ---------------------------- | ------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Toast message | `MESSAGE_KIND.TOAST` | [See payload](#toast-payload) | A notification should be shown to the user, e.g. when they have selected a fund. |
| Window dimension change | `MESSAGE_KIND.WINDOW_DIMENSION_CHANGE` | [See payload](#window-dimension-change-payload) | The DOM contents of the embed have changed and caused the height of the embed to change. |
| Loaded | `MESSAGE_KIND.LOADED` | _none_ | The embed has finished loading. |
| Employer settings updated¹ | `MESSAGE_KIND.EMPLOYER_SETTINGS_UPDATED` | _none_ | An update has been made to the employer settings but the change has not yet been delivered to the partner system. |
| Employer settings committed¹ | `MESSAGE_KIND.EMPLOYER_SETTINGS_COMMITTED` | _none_ | A change to the employer settings has been committed into the partner system (the partner webhook responded with a `<400` status code). |
| Onboarding intent completed | `MESSAGE_KIND.ONBOARDING_INTENT_COMPLETED` | _none_ | A user has completed their onboarding session intent. Useful for showing a "processing" state while waiting for the webhook to complete. |
| Onboarding session committed | `MESSAGE_KIND.ONBOARDING_SESSION_COMMITTED` | _none_ | A user has completed the onboarding flow and the data has been delivered into the partner system (the same "committed" rules as employer settings apply here). |
| Onboarding session finished | `MESSAGE_KIND.ONBOARDING_SESSION_FINISHED` | _none_ | A user has finished the onboarding flow but the payload has not yet been transmitted. Use this to move the user to the next step without waiting on async provisioning. |
| Onboarding step changed | `MESSAGE_KIND.ONBOARDING_STEP_CHANGED` | [See payload](#onboarding-step-changed-payload) | The embed has loaded, or the user has moved to the next step in the onboarding flow. |
| MFA verification completed | `MESSAGE_KIND.MFA_VERIFICATION_COMPLETED` | [See payload](#mfa-verification-completed-payload) | The user has finished their MFA session, either by verifying successfully or by exhausting the maximum number of verification attempts. |
| Page loaded | `MESSAGE_KIND.PAGE_LOADED` | _none_ | A new page is loaded inside the embed. The embed will also scroll the iFrame to the top of the viewport so the new page is visible. |
| Scroll into view | `MESSAGE_KIND.SCROLL_INTO_VIEW` | [See payload](#scroll-into-view-payload) | The embed has requested that the host page scroll so a position inside the iFrame becomes visible. The library smoothly scrolls the host so that position is at the top of the viewport. |

¹ Only fires for URLs which load the employer settings embed. The updated/committed pair can be used together to drive a busy/in-flight state in your UI.

Expand Down Expand Up @@ -153,6 +154,14 @@ If a sticky header on your host page is hiding the top of the iFrame after a `PA
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verified_at` | An ISO 8601 timestamp indicating when the MFA session was verified. Will be `null` when the maximum number of verification attempts has been exceeded, allowing you to distinguish a successful verification from an exhausted one. |

#### Scroll into view payload

| Name | Description |
| ----------- | ---------------------------------------------------------------------------------------------------------------------- |
| `offsetTop` | The vertical position inside the iFrame, in CSS pixels, that should be brought to the top of the host page's viewport. |

If your host page sets `scroll-padding-top` on the `html` element (e.g. for a sticky header), it is respected.

### Instance methods

The object returned from `new Embed({ ... })` exposes the following methods. Event callbacks receive the event's payload (the inner `data`), not the full message envelope; the payload shape for each event is described in the [Events](#events) section.
Expand Down
105 changes: 105 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,111 @@ describe("Embed", () => {
});
});
});

describe("scroll into view event", () => {
let scrollMarginAtCallTime: string | undefined;

beforeEach(() => {
scrollMarginAtCallTime = undefined;
Element.prototype.scrollIntoView = jest.fn(function (
this: HTMLElement,
) {
scrollMarginAtCallTime = this.style.scrollMarginTop;
});
});

it("logs the event", () => {
fireEvent(
window,
new MessageEvent("message", {
data: {
kind: MESSAGE_KIND.SCROLL_INTO_VIEW,
data: { offsetTop: 240 },
},
origin: "https://api.superapi.com.au",
}),
);

// eslint-disable-next-line @typescript-eslint/unbound-method
expect(loglevel.debug).toHaveBeenCalledWith(
"Reacting to scroll into view, scrolling host to iFrame offset 240",
);
});

it("calls externally bound listener", () => {
const listener = jest.fn();

const data = {
kind: MESSAGE_KIND.SCROLL_INTO_VIEW,
data: { offsetTop: 240 },
};

embed.on(MESSAGE_KIND.SCROLL_INTO_VIEW, listener);

fireEvent(
window,
new MessageEvent("message", {
data,
origin: "https://api.superapi.com.au",
}),
);

expect(listener).toHaveBeenCalledWith(data.data);
});

it("smooth-scrolls the iframe into view", () => {
fireEvent(
window,
new MessageEvent("message", {
data: {
kind: MESSAGE_KIND.SCROLL_INTO_VIEW,
data: { offsetTop: 240 },
},
origin: "https://api.superapi.com.au",
}),
);

// eslint-disable-next-line @typescript-eslint/unbound-method
expect(Element.prototype.scrollIntoView).toHaveBeenCalledWith({
behavior: "smooth",
block: "start",
});
});

it("applies a negative scrollMarginTop on the iframe for the scrollIntoView call", () => {
fireEvent(
window,
new MessageEvent("message", {
data: {
kind: MESSAGE_KIND.SCROLL_INTO_VIEW,
data: { offsetTop: 240 },
},
origin: "https://api.superapi.com.au",
}),
);

expect(scrollMarginAtCallTime).toBe("-240px");
});

it("restores the iframe scrollMarginTop after scrolling", () => {
const scope = within(element);
const iframe = scope.getByTestId("iframe");
iframe.style.scrollMarginTop = "16px";

fireEvent(
window,
new MessageEvent("message", {
data: {
kind: MESSAGE_KIND.SCROLL_INTO_VIEW,
data: { offsetTop: 240 },
},
origin: "https://api.superapi.com.au",
}),
);

expect(iframe.style.scrollMarginTop).toBe("16px");
});
});
});
});

Expand Down
24 changes: 24 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ import type {
Data as PageLoadedDataV1,
Message as PageLoadedMessageV1,
} from "./services/messages/v1/page_loaded";
import type {
Data as ScrollIntoViewDataV1,
Message as ScrollIntoViewMessageV1,
} from "./services/messages/v1/scroll_into_view";
import type {
Data as ToastMessageDataV1,
Kind as ToastKindV1,
Expand All @@ -60,6 +64,7 @@ export {
export { OnboardingSessionFinishedDataV1, OnboardingSessionFinishedMessageV1 };
export { OnboardingStepChangedDataV1, OnboardingStepChangedMessageV1 };
export { PageLoadedDataV1, PageLoadedMessageV1 };
export { ScrollIntoViewDataV1, ScrollIntoViewMessageV1 };
export { ToastKindV1, ToastMessageDataV1, ToastMessageV1 };
export { LoadedMessageDataV1, LoadedMessageV1 };
export { WindowDimensionChangeMessageDataV1, WindowDimensionChangeMessageV1 };
Expand All @@ -74,6 +79,7 @@ export type AvailableMessages =
| OnboardingSessionFinishedMessageV1
| OnboardingStepChangedMessageV1
| PageLoadedMessageV1
| ScrollIntoViewMessageV1
| ToastMessageV1
| WindowDimensionChangeMessageV1;

Expand All @@ -87,6 +93,7 @@ export type MessageKindToTypeMap = {
[MESSAGE_KIND.ONBOARDING_SESSION_FINISHED]: OnboardingSessionFinishedDataV1;
[MESSAGE_KIND.ONBOARDING_STEP_CHANGED]: OnboardingStepChangedDataV1;
[MESSAGE_KIND.PAGE_LOADED]: PageLoadedDataV1;
[MESSAGE_KIND.SCROLL_INTO_VIEW]: ScrollIntoViewDataV1;
[MESSAGE_KIND.TOAST]: ToastMessageDataV1;
[MESSAGE_KIND.WINDOW_DIMENSION_CHANGE]: WindowDimensionChangeMessageDataV1;
};
Expand Down Expand Up @@ -306,6 +313,23 @@ export class Embed {
break;
}

case MESSAGE_KIND.SCROLL_INTO_VIEW: {
const { offsetTop } = event.data.data;

log.debug(
`Reacting to scroll into view, scrolling host to iFrame offset ${offsetTop}`,
);

const previousScrollMargin = this.iframe.style.scrollMarginTop;
this.iframe.style.scrollMarginTop = `-${offsetTop}px`;
this.iframe.scrollIntoView({ behavior: "smooth", block: "start" });
this.iframe.style.scrollMarginTop = previousScrollMargin;

this.bus.emit(event.data.kind, event.data.data);

break;
}

case MESSAGE_KIND.TOAST: {
this.bus.emit(event.data.kind, event.data.data);
break;
Expand Down
1 change: 1 addition & 0 deletions src/services/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum MESSAGE_KIND {
ONBOARDING_STEP_CHANGED = "onboardingStepChanged",
ONBOARDING_SESSION_FINISHED = "onboardingSessionFinished",
PAGE_LOADED = "pageLoaded",
SCROLL_INTO_VIEW = "scrollIntoView",
TOAST = "toast",
WINDOW_DIMENSION_CHANGE = "windowDimensionChange",
}
11 changes: 11 additions & 0 deletions src/services/messages/v1/scroll_into_view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MESSAGE_KIND } from "../../messages";

export type Data = {
offsetTop: number;
};

export type Message = {
data: Data;
kind: MESSAGE_KIND.SCROLL_INTO_VIEW;
version: "v1";
};
Loading