UNPKG

@whop/iframe

Version:

Powers communication between Whop and your embedded app

580 lines (567 loc) 18.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { appsServerSchema: () => appsServerSchema, createSdk: () => createSdk, transport: () => transport_exports, whopServerSchema: () => whopServerSchema }); module.exports = __toCommonJS(index_exports); // src/sdk/apps-server.ts var import_zod2 = require("zod"); // src/sdk/utils.ts var import_zod = require("zod"); var withError = (schema, error) => { return import_zod.z.discriminatedUnion("status", [ import_zod.z.object({ status: import_zod.z.literal("ok"), data: schema }), import_zod.z.object({ status: import_zod.z.literal("error"), error }) ]); }; var frostedV2Theme = import_zod.z.object({ appearance: import_zod.z.enum(["light", "dark"]), accentColor: import_zod.z.string(), dangerColor: import_zod.z.string(), grayColor: import_zod.z.string(), infoColor: import_zod.z.string(), successColor: import_zod.z.string(), warningColor: import_zod.z.string() }).partial(); // src/sdk/apps-server.ts var appsServerSchema = import_zod2.z.discriminatedUnion("event", [ import_zod2.z.object({ event: import_zod2.z.literal("appPing"), request: import_zod2.z.literal("app_ping"), response: import_zod2.z.literal("app_pong") }), import_zod2.z.object({ event: import_zod2.z.literal("onColorThemeChange"), request: frostedV2Theme, response: import_zod2.z.void() }) ]); // src/sdk/mobile-app-postmessage.ts function getReactNativePostMessage() { const reactNativePostMessage = typeof window !== "undefined" && "ReactNativeWebView" in window && typeof window.ReactNativeWebView === "object" && window.ReactNativeWebView && "postMessage" in window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === "function" ? (data) => { if (typeof window !== "undefined" && "ReactNativeWebView" in window && typeof window.ReactNativeWebView === "object" && window.ReactNativeWebView && "postMessage" in window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === "function") window?.ReactNativeWebView?.postMessage(data); } : void 0; return reactNativePostMessage; } function getSwiftPostMessage() { const swiftMessageHandler = typeof window !== "undefined" && "webkit" in window && typeof window.webkit === "object" && window.webkit !== null && "messageHandlers" in window.webkit && typeof window.webkit.messageHandlers === "object" && window.webkit.messageHandlers !== null && "SwiftWebView" in window.webkit.messageHandlers && typeof window.webkit.messageHandlers.SwiftWebView === "object" && window.webkit.messageHandlers.SwiftWebView !== null && "postMessage" in window.webkit.messageHandlers.SwiftWebView ? window.webkit.messageHandlers.SwiftWebView : null; const swiftPostMessage = swiftMessageHandler ? (data) => { if (typeof swiftMessageHandler.postMessage === "function") { swiftMessageHandler.postMessage(data); } } : void 0; return swiftPostMessage; } // src/sdk/sync-href.ts function syncHref({ onChange }) { if (typeof window === "undefined") return; const initialHref = window.location.href; onChange({ href: initialHref }).catch(() => null); let lastKnown = initialHref; window.addEventListener("popstate", () => { const { href } = window.location; onChange({ href }).catch(() => null); lastKnown = href; }); if (window._whop_sync_href_interval) { clearInterval(window._whop_sync_href_interval); } window._whop_sync_href_interval = setInterval(() => { const { href } = window.location; if (href === lastKnown) return; onChange({ href }).catch(() => null); lastKnown = href; }, 250); } // src/sdk/transport/index.ts var transport_exports = {}; __export(transport_exports, { MESSAGE_TAG: () => MESSAGE_TAG, TimeoutError: () => TimeoutError, createHandler: () => createHandler, createSDK: () => createSDK, postmessageTransport: () => postmessageTransport }); // src/sdk/transport/utils.ts var TimeoutError = class extends Error { constructor() { super("Timeout"); } }; function randomId(length) { const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let str = ""; for (let i = 0; i < length; i++) { str += alphabet[Math.floor(Math.random() * alphabet.length)]; } return str; } // src/sdk/transport/sdk.ts function createSDK({ clientSchema, serverSchema, serverComplete, transport, timeout = 1e3, timeouts, localAppId, remoteAppId, serverImplementation = {}, serverMiddleware }) { const callbacks = []; const keys = clientSchema?.options.map( (option) => option._def.shape().event._def.value ) ?? []; const client = Object.fromEntries( keys.map((key) => [ key, async (req) => { const eventId = `${localAppId}:${key}:${randomId(8)}`; console.debug("[typed-transport] app. Created eventId", eventId); const responseData = new Promise((resolve, reject) => { const customTimeout = timeouts?.[key]; const timeoutId = setTimeout(() => { const index = callbacks.findIndex((cb) => cb.id === eventId); if (index !== -1) callbacks.splice(index, 1); if (serverComplete) { console.debug("[typed-transport] app. Timeout error"); reject(new TimeoutError()); } else resolve(void 0); }, customTimeout ?? timeout); if (customTimeout && customTimeout > timeout && !serverComplete) { const timeoutId2 = setTimeout(() => { const index = callbacks.findIndex((cb) => cb.id === eventId); if (index !== -1) callbacks.splice(index, 1); resolve(void 0); }, timeout); callbacks.push({ id: `${eventId}:processing`, resolve: () => clearTimeout(timeoutId2) }); } callbacks.push({ id: eventId, resolve: (data2) => { clearTimeout(timeoutId); resolve(data2); } }); }); console.debug("[typed-transport] app sending event", { eventId, localAppId, remoteAppId }); await transport.send?.(eventId, req, { localAppId, remoteAppId }); const data = await responseData; console.debug("[typed-transport] received response", data); return data; } ]) ); const cleanupRecv = transport.recv( async (event, dataAny) => { const [app, key, _randomId, type] = event.split(":"); if (app === localAppId) { const idx = callbacks.findIndex((cb2) => cb2.id === event); if (idx === -1) return; const dataSchema = clientSchema?.optionsMap.get(key); if (!dataSchema) return; const cb = callbacks[idx]; if (type === "processing") { cb.resolve(void 0); } else { const data = dataSchema.shape.response.parse(dataAny); callbacks.splice(idx, 1); cb.resolve(data); } } else if (app === remoteAppId) { if (serverImplementation === void 0) return; let handler = serverImplementation[key]; if (serverMiddleware) { for (let i = serverMiddleware.length - 1; i >= 0; i--) { const middlewareDef = serverMiddleware[i]; const middleware = middlewareDef[key]; if (!middleware) continue; const ref = handler; handler = (data2) => middleware(data2, ref); } } if (!handler) return; const dataSchema = serverSchema?.optionsMap.get(key); if (!dataSchema) return; const data = dataSchema.shape.request.parse(dataAny); const timeoutId = setTimeout(async () => { await transport.send( `${event}:processing`, {}, { localAppId, remoteAppId } ); }, 50); const response = await handler(data); clearTimeout(timeoutId); await transport.send(event, response, { localAppId, remoteAppId }); return response; } }, { localAppId, remoteAppId } ); const cleanupFunctions = []; if (transport.cleanup) cleanupFunctions.push(transport.cleanup); if (cleanupRecv) cleanupFunctions.push(cleanupRecv); client._cleanupTransport = () => { for (const fn of cleanupFunctions) fn(); }; return client; } // src/sdk/transport/handler.ts function createHandler({ schema, forceCompleteness, handlers }) { let eventHandler; createSDK({ clientSchema: void 0, serverSchema: schema, localAppId: "client", remoteAppId: "server", forceCompleteness, serverImplementation: handlers, transport: { send() { }, recv(handler) { eventHandler = handler; } } }); return (event, data) => { return eventHandler(`server:${event}`, data); }; } // src/sdk/transport/postmessage.ts var MESSAGE_TAG = "typed-transport"; function postmessageTransport({ remoteWindow, targetOrigins }) { return { send(event, data, { remoteAppId, localAppId }) { if (!remoteWindow) { throw new Error( "No remote window. Is the SDK running on a server without a global window object?" ); } console.debug( "[typed-transport] postmessagetransport. Sending event", event, data ); console.debug( "[typed-transport] postmessagetransport. target origins =", targetOrigins ); for (const targetOrigin of targetOrigins) { console.debug("[typed-transport] remoteWindow.postMessage", { event, libId: MESSAGE_TAG, receiverAppId: remoteAppId, senderAppId: localAppId }); console.debug( "[typed-transport] remoteWindow.postMessage.data", data, JSON.stringify(data) ); remoteWindow.postMessage( { event, data, libId: MESSAGE_TAG, receiverAppId: remoteAppId, senderAppId: localAppId }, { targetOrigin } ); } if (targetOrigins.length === 0) { remoteWindow.postMessage({ event, data, libId: MESSAGE_TAG, receiverAppId: remoteAppId, senderAppId: localAppId }); } }, recv(handler, { localAppId, remoteAppId }) { const listener = (event) => { console.debug( "[typed-transport] postmessagetransport. Receiving event", event ); if (event.source !== remoteWindow || !targetOrigins.includes(event.origin) && targetOrigins.length > 0 || !event.data || !event.data.event || event.data.libId !== MESSAGE_TAG || event.data.receiverAppId !== localAppId || event.data.senderAppId !== remoteAppId) { return; } handler(event.data.event, event.data.data); }; if (typeof window === "undefined") { return; } window.addEventListener("message", listener); return () => { window.removeEventListener("message", listener); }; } }; } function reactNativeClientTransport({ postMessage, targetOrigin }) { return { send(event, data, { remoteAppId, localAppId }) { postMessage( JSON.stringify({ event, data, libId: MESSAGE_TAG, receiverAppId: remoteAppId, senderAppId: localAppId }) ); }, recv(handler, { localAppId, remoteAppId }) { const listener = (event) => { const dataString = typeof event.data === "string" ? event.data : null; if (!dataString) return; const data = JSON.parse(dataString); if (event.origin !== targetOrigin || !data || !data.event || !data.data || data.libId !== MESSAGE_TAG || data.receiverAppId !== localAppId || data.senderAppId !== remoteAppId) { return; } handler(data.event, data.data); }; if (typeof window === "undefined") { console.warn( "No window. Is the SDK running on a server without a global window object?" ); return; } window.addEventListener("message", listener); return () => { window.removeEventListener("message", listener); }; } }; } // src/sdk/whop-server.ts var import_zod3 = require("zod"); var whopServerSchema = import_zod3.z.discriminatedUnion("event", [ import_zod3.z.object({ event: import_zod3.z.literal("ping"), request: import_zod3.z.literal("ping"), response: import_zod3.z.literal("pong") }), import_zod3.z.object({ event: import_zod3.z.literal("getTopLevelUrlData"), request: import_zod3.z.object({}).optional(), response: import_zod3.z.object({ companyRoute: import_zod3.z.string(), experienceRoute: import_zod3.z.string(), experienceId: import_zod3.z.string(), viewType: import_zod3.z.enum(["app", "admin", "analytics", "preview"]), baseHref: import_zod3.z.string(), fullHref: import_zod3.z.string() }) }), import_zod3.z.object({ event: import_zod3.z.literal("openExternalUrl"), request: import_zod3.z.object({ newTab: import_zod3.z.boolean().optional(), url: import_zod3.z.string() }), response: import_zod3.z.literal("ok") }), import_zod3.z.object({ event: import_zod3.z.literal("onHrefChange"), request: import_zod3.z.object({ href: import_zod3.z.string() }), response: import_zod3.z.literal("ok") }), import_zod3.z.object({ event: import_zod3.z.literal("inAppPurchase"), request: import_zod3.z.object({ /** * ID returned from the `chargeUser` API call. * @example "ch_1234567890" */ id: import_zod3.z.string().optional(), /** * ID of the plan returned from the `chargeUser` API call. * @example "plan_1234567890" */ planId: import_zod3.z.string() }), response: withError( import_zod3.z.object({ sessionId: import_zod3.z.string(), /** * The receipt ID can be used to verify the purchase. * * NOTE: When receiving payments you should always listen to webhooks as a fallback * to process the payment. Do not solely rely on the client to process payments. The receipt ID * can be used to deduplicate payment events. */ receiptId: import_zod3.z.string() }), import_zod3.z.string() ) }), import_zod3.z.object({ event: import_zod3.z.literal("closeApp"), request: import_zod3.z.null(), response: import_zod3.z.literal("ok") }), import_zod3.z.object({ event: import_zod3.z.literal("openHelpChat"), request: import_zod3.z.null(), response: import_zod3.z.literal("ok") }), import_zod3.z.object({ event: import_zod3.z.literal("getColorTheme"), request: import_zod3.z.void(), response: frostedV2Theme }), import_zod3.z.object({ event: import_zod3.z.literal("earliestUnreadNotification"), request: import_zod3.z.object({ experienceId: import_zod3.z.string() }), response: import_zod3.z.object({ externalId: import_zod3.z.string() }).nullable() }), import_zod3.z.object({ event: import_zod3.z.literal("markExperienceRead"), request: import_zod3.z.object({ experienceId: import_zod3.z.string(), notificationExternalId: import_zod3.z.string().optional() }), response: import_zod3.z.literal("ok") }), import_zod3.z.object({ event: import_zod3.z.literal("performHaptic"), request: import_zod3.z.object({ type: import_zod3.z.enum(["selection", "impact", "notification"]), style: import_zod3.z.enum(["light", "medium", "heavy"]) }), response: import_zod3.z.literal("ok") }) ]); // src/sdk/index.ts function setColorTheme(theme) { document.documentElement.dispatchEvent( new CustomEvent("frosted-ui:set-theme", { detail: theme }) ); } function createSdk({ onMessage = {}, appId = process.env.NEXT_PUBLIC_WHOP_APP_ID, overrideParentOrigins }) { const mobileWebView = getSwiftPostMessage() ?? getReactNativePostMessage(); const remoteWindow = typeof window === "undefined" ? void 0 : window.parent; if (!appId) { throw new Error( "[createSdk]: appId is required. Please provide an appId or set the NEXT_PUBLIC_WHOP_APP_ID environment variable." ); } const sdk = createSDK({ clientSchema: whopServerSchema, serverSchema: appsServerSchema, forceCompleteness: false, serverImplementation: onMessage, localAppId: appId, remoteAppId: "app_whop", transport: mobileWebView ? reactNativeClientTransport({ postMessage: mobileWebView, targetOrigin: "com.whop.whopapp" }) : postmessageTransport({ remoteWindow, targetOrigins: overrideParentOrigins ?? [ "https://whop.com", "https://dash.whop.com", "http://localhost:8003" ] }), serverComplete: true, serverMiddleware: [ { onColorThemeChange: setColorTheme } ], timeout: 15e3, timeouts: { inAppPurchase: 1e3 * 60 * 60 * 24, // 24 hours, we never want this to timeout. onHrefChange: 500 // we don't really care about a response here. } }); if (typeof window !== "undefined") { sdk.getColorTheme().then(setColorTheme).catch(() => null); document.documentElement.addEventListener("frosted-ui:mounted", () => { sdk.getColorTheme().then(setColorTheme).catch(() => null); }); } syncHref({ onChange: sdk.onHrefChange }); return sdk; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { appsServerSchema, createSdk, transport, whopServerSchema }); //# sourceMappingURL=index.js.map