@whop/iframe
Version:
Powers communication between Whop and your embedded app
580 lines (567 loc) • 18.8 kB
JavaScript
;
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