Skip to content

Commit

Permalink
Merge pull request #816 from magiclabs/khamdam-sc-PDEEXP-1908-Iframe-…
Browse files Browse the repository at this point in the history
…heartbeat

feat: add iframe heartbeat implementation.
  • Loading branch information
joshuascan authored Oct 22, 2024
2 parents 69206d9 + 7c91deb commit 85ed954
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 53 deletions.
17 changes: 16 additions & 1 deletion packages/magic-sdk/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
export { default } from '../../jest.config';
import JestConfig from '../../jest.config';
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
...JestConfig,

coverageThreshold: {
global: {
lines: 70,
statements: 70,
functions: 55,
branches: 70,
},
},
};
export default config;
77 changes: 76 additions & 1 deletion packages/magic-sdk/src/iframe-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* eslint-disable no-unused-expressions */

import { ViewController, createDuplicateIframeWarning, createURL, createModalNotReadyError } from '@magic-sdk/provider';
import { MagicIncomingWindowMessage, MagicOutgoingWindowMessage } from '@magic-sdk/types';

/**
* Magic `<iframe>` overlay styles. These base styles enable `<iframe>` UI
Expand Down Expand Up @@ -45,13 +46,25 @@ function checkForSameSrcInstances(parameters: string) {
return Boolean(iframes.find((iframe) => iframe.src.includes(parameters)));
}

const SECOND = 1000;
const MINUTE = 60 * SECOND;
const RESPONSE_DELAY = 15 * SECOND; // 15 seconds
const PING_INTERVAL = 2 * MINUTE; // 2 minutes
const INITIAL_HEARTBEAT_DELAY = 60 * MINUTE; // 1 hour

/**
* View controller for the Magic `<iframe>` overlay.
*/
export class IframeController extends ViewController {
private iframe!: Promise<HTMLIFrameElement>;
private activeElement: any = null;
private lastPingTime = Date.now();
private intervalTimer: ReturnType<typeof setInterval> | null = null;
private timeoutTimer: ReturnType<typeof setTimeout> | null = null;

private getIframeSrc() {
return createURL(`/send?params=${encodeURIComponent(this.parameters)}`, this.endpoint).href;
}
/**
* Initializes the underlying `<iframe>` element.
* Initializes the underlying `Window.onmessage` event listener.
Expand All @@ -65,7 +78,7 @@ export class IframeController extends ViewController {
iframe.classList.add('magic-iframe');
iframe.dataset.magicIframeLabel = createURL(this.endpoint).host;
iframe.title = 'Secure Modal';
iframe.src = createURL(`/send?params=${encodeURIComponent(this.parameters)}`, this.endpoint).href;
iframe.src = this.getIframeSrc();
iframe.allow = 'clipboard-read; clipboard-write';
applyOverlayStyles(iframe);
document.body.appendChild(iframe);
Expand All @@ -84,9 +97,22 @@ export class IframeController extends ViewController {
}
});

this.iframe.then((iframe) => {
if (iframe instanceof HTMLIFrameElement) {
iframe.addEventListener('load', async () => {
await this.startHeartBeat();
});
}
});

window.addEventListener('message', (event: MessageEvent) => {
if (event.origin === this.endpoint) {
if (event.data && event.data.msgType && this.messageHandlers.size) {
const isPongMessage = event.data.msgType.includes(MagicIncomingWindowMessage.MAGIC_PONG);

if (isPongMessage) {
this.lastPingTime = Date.now();
}
// If the response object is undefined, we ensure it's at least an
// empty object before passing to the event listener.
/* istanbul ignore next */
Expand All @@ -97,6 +123,10 @@ export class IframeController extends ViewController {
}
}
});

window.addEventListener('beforeunload', () => {
this.stopHeartBeat();
});
}

protected async showOverlay() {
Expand All @@ -123,4 +153,49 @@ export class IframeController extends ViewController {
throw createModalNotReadyError();
}
}

private heartBeatCheck() {
this.intervalTimer = setInterval(async () => {
const message = { msgType: `${MagicOutgoingWindowMessage.MAGIC_PING}-${this.parameters}`, payload: [] };

await this._post(message);

const timeSinceLastPing = Date.now() - this.lastPingTime;

if (timeSinceLastPing > RESPONSE_DELAY) {
await this.reloadIframe();
this.lastPingTime = Date.now();
}
}, PING_INTERVAL);
}

private async startHeartBeat() {
const iframe = await this.iframe;

if (iframe) {
this.timeoutTimer = setTimeout(() => this.heartBeatCheck(), INITIAL_HEARTBEAT_DELAY);
}
}

private stopHeartBeat() {
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}

if (this.intervalTimer) {
clearInterval(this.intervalTimer);
this.intervalTimer = null;
}
}

private async reloadIframe() {
const iframe = await this.iframe;

if (iframe) {
iframe.src = this.getIframeSrc();
} else {
throw createModalNotReadyError();
}
}
}
Loading

0 comments on commit 85ed954

Please sign in to comment.