@dynamic-labs/iframe-setup
A utility package that enables the Dynamic SDK (@dynamic-labs/sdk-react-core) to work seamlessly within an iframe environment.
Installation
npm install @dynamic-labs/iframe-setup
Features
This package provides the setupIframe method thats used to setup the iframe environment.
Implementation Guide
Step 1: Parent Page Setup
Configure the parent page that will contain the iframe.
To setup the iframe, you need to pass the parent URL to the iframe using the initial-parent-url query parameter
import { setupIframe } from '@dynamic-labs/iframe-setup';
const iframeURL = new URL('https://iframe.com');
if (typeof window !== 'undefined') {
iframeURL.searchParams.set('initial-parent-url', window.location.href);
}
const App = () => {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
const cleanUp = setupIframe(iframeRef.current!, iframeURL.origin);
return () => {
cleanUp();
};
}, []);
return (
<div>
<iframe ref={iframeRef} src={iframeURL.toString()} />
</div>
);
};
Step 2: Iframe Application Setup
Inside your iframe application, initialize the SDK:
import { DynamicContextProvider } from '@dynamic-labs/sdk-react-core';
import { setupInsideIframe } from '@dynamic-labs/utils';
// Initialize as early as possible in your iframe application
setupInsideIframe();
const App = () => {
return (
<DynamicContextProvider>{/* Your application */}</DynamicContextProvider>
);
};
Once both steps are completed, the Dynamic SDK will be fully operational within your iframe environment.
Enabling Social Redirect
To enable social redirect functionality within your iframe application, you need to configure the redirectUrl setting in your DynamicContextProvider.
import { DynamicContextProvider } from '@dynamic-labs/sdk-react-core';
import { PlatformService } from '@dynamic-labs/utils';
import { setupInsideIframe } from '@dynamic-labs/utils';
// Initialize as early as possible in your iframe application
setupInsideIframe();
const App = () => {
return (
<DynamicContextProvider
settings={{
redirectUrl: PlatformService.getUrl().toString(),
// other settings
}}
>
{/* Your application */}
</DynamicContextProvider>
);
};
This configuration ensures that after social authentication, users are properly redirected back to your application and not the iframe URL.
Alternative Implementation
Manual Setup (Without Package Installation in Parent App)
If you're unable to install the package in the parent application, you can directly implement the setupIframe functionality by copying the code below:
/**
* Setup an iframe to communicate with the Dynamic SDK
* running inside the iframe
*
* This file is self contained and should not import or depend on any
* other file or package.
*
* @see https://www.npmjs.com/package/@dynamic-labs/iframe-setup
*/
/* eslint no-restricted-syntax: ["error", "ImportDeclaration"] */
/**
* The same implementation was added to the utils package
* and they should be kept in sync
* The type is duplicated here to avoid a dependency on the utils package
* @see packages/utils/src/setupInsideIframe/types.ts
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type MESSAGE_HANDLER = (...params: any[]) => Promise<any> | void;
type IFRAME_EVENTS = {
OPEN_URL: (args: {
url: string;
target?: 'self' | 'blank';
features?: string;
}) => void;
UPDATE_PARENT_URL: (args: { url: string }) => void;
APP_FOCUS: () => void;
};
/**
* Setup an iframe to comunicate with the Dynamic SDK
* running inside the iframe
*
* @returns a function to teardown the iframe setup
*/
export const setupIframe = (
iframe: HTMLIFrameElement,
origin: string,
): VoidFunction => {
if (typeof window === 'undefined') return () => {};
const messageHandler = createMessageHandler<IFRAME_EVENTS>(
window,
origin,
iframe,
);
const messageSender = createMessageSender<IFRAME_EVENTS>(iframe);
// Add event listener to handle deeplink
const setupOpenUrlHandler = () =>
messageHandler('OPEN_URL', ({ url, target = 'self', features }) => {
// This navigates the host top window, so never honor a URL whose scheme
// can execute script in the host origin (iframe->host XSS).
if (hasDangerousScheme(url)) return;
if (target === 'blank') {
window.open(url, '_blank', features);
} else {
window.location.assign(url);
}
});
// Watch for url changes and update Dynamic SDK in iframe
const setupUrlUpdateHandler = (): VoidFunction => {
const notifyUrlChange = () => {
try {
messageSender('UPDATE_PARENT_URL', { url: window.location.href });
} catch {
// Never let a notification failure surface to the host application
// (e.g. an SPA router calling history.pushState)
}
};
patchHistoryOnce();
// popstate fires on browser back/forward navigation; the custom event
// fires on programmatic pushState/replaceState (which never emit popstate)
window.addEventListener('popstate', notifyUrlChange);
window.addEventListener(URL_CHANGE_EVENT, notifyUrlChange);
return () => {
window.removeEventListener('popstate', notifyUrlChange);
window.removeEventListener(URL_CHANGE_EVENT, notifyUrlChange);
};
};
/**
* Watch for app focus and send event to Dynamic SDK in iframe
*/
const watchForAppVisibilityChange = (): VoidFunction => {
let lastFired = false;
const onVisibilityOrFocus = () => {
if (!lastFired && document.visibilityState === 'visible') {
lastFired = true;
messageSender('APP_FOCUS');
// Reset flag after a short delay to allow next valid trigger
setTimeout(() => (lastFired = false), 100);
}
};
document.addEventListener('visibilitychange', onVisibilityOrFocus);
window.addEventListener('focus', onVisibilityOrFocus);
return () => {
document.removeEventListener('visibilitychange', onVisibilityOrFocus);
window.removeEventListener('focus', onVisibilityOrFocus);
};
};
// Holds all the cleanup handlers
const cleanupHandlers: VoidFunction[] = [];
cleanupHandlers.push(setupUrlUpdateHandler());
cleanupHandlers.push(setupOpenUrlHandler());
cleanupHandlers.push(watchForAppVisibilityChange());
return () => cleanupHandlers.forEach((offHandler) => offHandler());
};
// Utils
/**
* URL schemes that can execute script in the host origin. Navigating the host
* top window to any of these turns an OPEN_URL message into arbitrary code
* execution in the integrator's origin, so they are blocked.
*
* A deny-list (rather than an http/https allow-list) is used deliberately:
* wallet flows legitimately open custom-scheme deep links, and allow-listing
* http(s) would break them.
*/
const DANGEROUS_SCHEMES = [
'javascript:',
'data:',
'blob:',
'vbscript:',
'file:',
];
/**
* Leading characters the browser ignores when resolving a URL scheme: spaces,
* tabs, newlines and other C0 control chars (U+0000–U+0020). Stripping them
* defeats obfuscation such as "\tjavascript:alert(1)".
*/
// eslint-disable-next-line no-control-regex
const LEADING_IGNORED_CHARS = /^[\u0000-\u0020]+/;
/**
* Returns true when the URL begins with a script-executing scheme.
*
* Non-string input is treated as dangerous, leading ignored chars are stripped
* first, and the comparison is case-insensitive ("JavaScript:" is blocked).
*/
const hasDangerousScheme = (url: unknown): boolean => {
if (typeof url !== 'string') return true;
const normalized = url.replace(LEADING_IGNORED_CHARS, '').toLowerCase();
return DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme));
};
/**
* Custom event dispatched when the parent URL changes via the History API.
* Used instead of re-patching history per setupIframe call so that multiple
* instances can observe changes without clobbering each other's patches.
*/
const URL_CHANGE_EVENT = 'dynamic-labs:url-change';
const HISTORY_PATCHED_FLAG = '__dynamicLabsHistoryPatched';
/**
* Patch history.pushState/replaceState to emit URL_CHANGE_EVENT.
*
* popstate does not fire for programmatic navigation, so we patch the History
* API to broadcast a custom event instead. The patch is installed only once,
* retains no references, and is intentionally never restored — it merely
* dispatches an event, which keeps it safe to leave in place and composable
* across multiple setupIframe instances.
*/
const patchHistoryOnce = (): void => {
const history = window.history as History & Record<string, unknown>;
if (history[HISTORY_PATCHED_FLAG]) return;
history[HISTORY_PATCHED_FLAG] = true;
const originalPushState = history.pushState.bind(history);
const originalReplaceState = history.replaceState.bind(history);
history.pushState = (...args: Parameters<History['pushState']>) => {
originalPushState(...args);
window.dispatchEvent(new Event(URL_CHANGE_EVENT));
};
history.replaceState = (...args: Parameters<History['replaceState']>) => {
originalReplaceState(...args);
window.dispatchEvent(new Event(URL_CHANGE_EVENT));
};
};
/**
* Create a message handler for the iframe
*/
const createMessageHandler =
<T extends Record<string, MESSAGE_HANDLER>>(
window: Window,
origin: string,
iframe: HTMLIFrameElement,
) =>
<EVENT_NAME extends Extract<keyof T, string>>(
eventName: EVENT_NAME,
handler: T[EVENT_NAME],
): VoidFunction => {
const messageHandler = (event: MessageEvent) => {
if (event.origin !== origin) return;
// Only trust messages coming from this iframe's own window. Checking the
// origin alone lets any frame/tab at the SDK-iframe origin forge events.
if (event.source !== iframe.contentWindow) return;
const { eventName: incomingEventName, args } = event.data;
if (eventName === incomingEventName) {
handler(...args);
}
};
window.addEventListener('message', messageHandler);
return () => {
window.removeEventListener('message', messageHandler);
};
};
/**
* Create a function to send messages to the iframe
*/
const createMessageSender =
<T extends Record<string, MESSAGE_HANDLER>>(iframe: HTMLIFrameElement) =>
<EVENT_NAME extends Extract<keyof T, string>>(
eventName: EVENT_NAME,
...args: Parameters<T[EVENT_NAME]>
) => {
iframe.contentWindow?.postMessage(
{ args, eventName },
new URL(iframe.src).origin,
);
};
Now you can import and use the setupIframe function in your iframe application:
import { setupIframe } from './iframe-setup';
const iframeURL = new URL('https://iframe.com');
if (typeof window !== 'undefined') {
iframeURL.searchParams.set('initial-parent-url', window.location.href);
}
const App = () => {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
const cleanUp = setupIframe(iframeRef.current!, iframeURL.origin);
return () => {
cleanUp();
};
}, []);
return (
<div>
<iframe ref={iframeRef} src={iframeURL.toString()} />
</div>
);
};