UNPKG

@dollardonationclub/impactapi-js

Version:

JavaScript SDK for integrating the DDC Impact Widget into any website

495 lines (489 loc) 16.5 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // ../shared/dist/utils.js function isWidgetMessage(data) { if (!data || typeof data !== "object") { return false; } const msg = data; return typeof msg.type === "string" && typeof msg.timestamp === "number" && typeof msg.sessionId === "string"; } // ../shared/dist/errors.js var WidgetErrorCode; (function(WidgetErrorCode2) { WidgetErrorCode2["MOUNT_FAILED"] = "MOUNT_FAILED"; WidgetErrorCode2["CONTAINER_NOT_FOUND"] = "CONTAINER_NOT_FOUND"; WidgetErrorCode2["ALREADY_MOUNTED"] = "ALREADY_MOUNTED"; WidgetErrorCode2["INITIALIZATION_FAILED"] = "INITIALIZATION_FAILED"; WidgetErrorCode2["INVALID_CONFIG"] = "INVALID_CONFIG"; WidgetErrorCode2["MISSING_WIDGET_ID"] = "MISSING_WIDGET_ID"; WidgetErrorCode2["MISSING_SECRET"] = "MISSING_SECRET"; WidgetErrorCode2["API_ERROR"] = "API_ERROR"; WidgetErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR"; WidgetErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED"; WidgetErrorCode2["SESSION_EXPIRED"] = "SESSION_EXPIRED"; WidgetErrorCode2["MESSAGE_TIMEOUT"] = "MESSAGE_TIMEOUT"; WidgetErrorCode2["INVALID_MESSAGE"] = "INVALID_MESSAGE"; WidgetErrorCode2["WIDGET_NOT_READY"] = "WIDGET_NOT_READY"; WidgetErrorCode2["OPERATION_FAILED"] = "OPERATION_FAILED"; WidgetErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR"; })(WidgetErrorCode || (WidgetErrorCode = {})); var WidgetError = class _WidgetError extends Error { constructor(code, message, options) { super(message); Object.defineProperty(this, "code", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "recoverable", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "details", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "cause", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.name = "WidgetError"; this.code = code; this.recoverable = options?.recoverable ?? false; this.details = options?.details; this.cause = options?.cause; if (typeof Error.captureStackTrace === "function") { Error.captureStackTrace(this, _WidgetError); } } /** * Converts the error to a plain object for serialization */ toJSON() { return { name: this.name, code: this.code, message: this.message, recoverable: this.recoverable, details: this.details, stack: this.stack }; } }; var WidgetErrors = { mountFailed: (message, cause) => new WidgetError(WidgetErrorCode.MOUNT_FAILED, message, { recoverable: true, cause }), containerNotFound: (selector) => new WidgetError(WidgetErrorCode.CONTAINER_NOT_FOUND, `Container not found: ${selector}`, { recoverable: false }), invalidConfig: (message, details) => new WidgetError(WidgetErrorCode.INVALID_CONFIG, message, { recoverable: false, details }), apiError: (message, details, cause) => new WidgetError(WidgetErrorCode.API_ERROR, message, { recoverable: true, details, cause }), unauthorized: (message = "Unauthorized: Invalid widget secret") => new WidgetError(WidgetErrorCode.UNAUTHORIZED, message, { recoverable: false }), timeout: (operation, timeout) => new WidgetError(WidgetErrorCode.MESSAGE_TIMEOUT, `${operation} timed out after ${timeout}ms`, { recoverable: true, details: { operation, timeout } }), notReady: (operation) => new WidgetError(WidgetErrorCode.WIDGET_NOT_READY, `Cannot ${operation}: widget not ready`, { recoverable: true }) }; // ../shared/dist/constants.js var TIMING = { /** Default timeout for widget operations (10 seconds) */ DEFAULT_TIMEOUT_MS: 1e4 }; // src/config.ts var WIDGET_URL = "https://embed.impactapi.co"; // src/widget-client.ts function createWidget(config) { return new WidgetClient(config); } function createPreviewWidget(config) { return new PreviewWidgetClient(config); } var BaseWidgetClient = class { constructor(styleMode) { __publicField(this, "iframe", null); __publicField(this, "container", null); __publicField(this, "eventHandlers", /* @__PURE__ */ new Map()); __publicField(this, "widgetUrl"); __publicField(this, "targetOrigin"); __publicField(this, "styleMode"); __publicField(this, "debug"); __publicField(this, "isWidgetReady", false); __publicField(this, "pendingMessages", []); __publicField(this, "sessionData", null); __publicField(this, "currentAllocations", []); __publicField(this, "messageListener", null); __publicField(this, "readyTimeoutId", null); // Handshake state for widget-ready-for-init protocol __publicField(this, "widgetReadyForInit", false); __publicField(this, "initMessagePending", false); this.widgetUrl = WIDGET_URL; this.styleMode = styleMode; try { const url = new URL(this.widgetUrl); this.targetOrigin = url.origin; } catch (e) { console.error("[DDC Widget] Invalid WIDGET_URL, cannot determine targetOrigin"); throw WidgetErrors.invalidConfig("Invalid WIDGET_URL format"); } this.debug = false; this.setupMessageListener(); } async mount(container) { if (this.iframe) { this.log("Widget already mounted, destroying previous instance"); this.destroy(); } const containerEl = typeof container === "string" ? document.getElementById(container) || document.querySelector(container) : container; if (!containerEl) { throw WidgetErrors.containerNotFound(typeof container === "string" ? container : "[HTMLElement]"); } this.container = containerEl; try { this.createIframe(); await this.waitForReady(); } catch (error) { const widgetError = error instanceof WidgetError ? error : WidgetErrors.mountFailed("Failed to mount widget", error); this.log("Error mounting widget:", widgetError); throw widgetError; } } destroy() { this.log("Destroying widget"); if (this.readyTimeoutId !== null) { clearTimeout(this.readyTimeoutId); this.readyTimeoutId = null; } if (this.iframe?.contentWindow && this.isWidgetReady) { this.sendMessageToWidget({ type: "destroy" }); } if (this.iframe?.parentNode) { this.iframe.parentNode.removeChild(this.iframe); } if (this.messageListener) { window.removeEventListener("message", this.messageListener); this.messageListener = null; } this.iframe = null; this.container = null; this.isWidgetReady = false; this.pendingMessages = []; this.widgetReadyForInit = false; this.initMessagePending = false; } isReady() { return this.isWidgetReady; } getSessionData() { return this.sessionData; } waitFor(event, options) { return new Promise((resolve, reject) => { const timeout = options?.timeout || TIMING.DEFAULT_TIMEOUT_MS; const timer = setTimeout(() => { this.off(event, handler); reject(WidgetErrors.timeout(`waitFor('${String(event)}')`, timeout)); }, timeout); const handler = (data) => { clearTimeout(timer); this.off(event, handler); resolve(data); }; this.on(event, handler); }); } on(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, /* @__PURE__ */ new Set()); } const handlers = this.eventHandlers.get(event); if (handlers) { handlers.add(handler); } this.log(`Registered handler for event: ${String(event)}`); return this; } off(event, handler) { if (!handler) { this.eventHandlers.delete(event); this.log(`Removed all handlers for event: ${String(event)}`); } else { const handlers = this.eventHandlers.get(event); if (handlers) { handlers.delete(handler); this.log(`Removed specific handler for event: ${String(event)}`); } } return this; } emit(event, data) { const handlers = this.eventHandlers.get(event); if (handlers && handlers.size > 0) { this.log(`Emitting event: ${String(event)}`, data); handlers.forEach((handler) => { try { handler(data); } catch (error) { this.log(`Error in event handler for ${String(event)}:`, error); } }); } } createIframe() { if (!this.container) { throw WidgetErrors.mountFailed("Container not set"); } this.iframe = document.createElement("iframe"); this.iframe.src = this.buildIframeUrl(); this.iframe.style.width = "100%"; this.iframe.style.height = "100%"; this.iframe.style.border = "none"; this.iframe.title = "DDC Impact Widget"; this.container.appendChild(this.iframe); this.iframe.addEventListener("load", () => { this.sendInitMessage(); }); this.log("Iframe created with URL:", this.iframe.src); } setupMessageListener() { this.messageListener = (event) => { if (!this.iframe || event.source !== this.iframe.contentWindow) { return; } if (event.data?.type === "widget-ready-for-init") { this.log("Received widget-ready-for-init signal"); this.widgetReadyForInit = true; if (this.initMessagePending) { this.doSendInitMessage(); } return; } try { if (!isWidgetMessage(event.data)) { this.log("Received invalid message format, ignoring"); return; } const message = event.data; this.log("Received message:", message); if (message.type === "ready") { this.isWidgetReady = true; this.processPendingMessages(); } if (message.type === "resize") { this.handleResize(message.payload); } if (message.type === "session-updated") { this.sessionData = message.payload; this.currentAllocations = this.sessionData.allocations || []; } if (message.type === "allocations-updated") { const allocationsData = message.payload; this.currentAllocations = allocationsData.allocations; } this.emit(message.type, message.payload); } catch (error) { this.log("Error processing message:", error); } }; window.addEventListener("message", this.messageListener); } handleResize(data) { if (!this.iframe) return; if (data.height !== void 0) { this.iframe.style.height = `${data.height}px`; this.log("Iframe height updated to:", data.height); } if (data.width !== void 0) { this.iframe.style.width = `${data.width}px`; this.log("Iframe width updated to:", data.width); } } processPendingMessages() { while (this.pendingMessages.length > 0 && this.isWidgetReady) { const message = this.pendingMessages.shift(); if (message) { this.sendMessageToWidget(message); } } } sendMessageToWidget(message) { if (!this.iframe?.contentWindow) { this.log("No iframe content window available"); return; } if (!this.isWidgetReady && message.type !== "init" && message.type !== "preview-init") { this.pendingMessages.push(message); this.log("Widget not ready, queuing message:", message); return; } this.log("Sending message to widget:", message); this.iframe.contentWindow.postMessage(message, this.targetOrigin); } waitForReady() { return new Promise((resolve, reject) => { if (this.isWidgetReady) { resolve(this.getSessionId()); return; } const timeout = setTimeout(() => { cleanup(); reject(WidgetErrors.timeout("Widget ready", TIMING.DEFAULT_TIMEOUT_MS)); }, TIMING.DEFAULT_TIMEOUT_MS); this.readyTimeoutId = timeout; const readyHandler = (data) => { cleanup(); resolve(data.sessionId); }; this.on("ready", readyHandler); const cleanup = () => { clearTimeout(timeout); this.readyTimeoutId = null; this.off("ready", readyHandler); }; }); } log(message, data) { if (this.debug) { console.log(`[DDC Widget SDK] ${message}`, data !== void 0 ? data : ""); } } }; var WidgetClient = class extends BaseWidgetClient { constructor(config) { super(config.styleMode); __publicField(this, "sessionId"); __publicField(this, "secret"); if (!config.sessionId || typeof config.sessionId !== "string" || config.sessionId.trim() === "") { throw WidgetErrors.invalidConfig("sessionId must be a non-empty string"); } if (!config.secret || typeof config.secret !== "string" || config.secret.trim() === "") { throw WidgetErrors.invalidConfig("secret must be a non-empty string"); } this.sessionId = config.sessionId; this.secret = config.secret; } getSessionId() { return this.sessionId; } getType() { return this.sessionData?.type ?? null; } getAllocations() { return this.currentAllocations; } async refresh() { this.log("Refreshing session data..."); this.sendMessageToWidget({ type: "refresh" }); await this.waitFor("session-updated", { timeout: TIMING.DEFAULT_TIMEOUT_MS }); this.log("Session data refreshed"); } buildIframeUrl() { const url = new URL(this.widgetUrl); url.searchParams.set("sessionId", this.sessionId); if (this.styleMode) { url.searchParams.set("styleMode", this.styleMode); } return url.toString(); } sendInitMessage() { this.initMessagePending = true; if (this.widgetReadyForInit) { this.doSendInitMessage(); } else { this.log("Waiting for widget-ready-for-init signal..."); } } doSendInitMessage() { this.initMessagePending = false; this.log("Sending init message to widget"); this.sendMessageToWidget({ type: "init", secret: this.secret }); } }; var PreviewWidgetClient = class extends BaseWidgetClient { constructor(config) { super(config.styleMode); __publicField(this, "previewConfig"); __publicField(this, "previewSessionId"); if (config.type === "add_on") { this.previewConfig = { type: config.type, amount: config.amount, available_campaigns: config.available_campaigns }; } else if (config.type === "portion_of_sales_choice") { this.previewConfig = { type: config.type, amount: config.amount, available_campaigns: config.available_campaigns }; } else { this.previewConfig = { type: config.type, allocations: config.allocations }; } this.previewSessionId = ""; } getSessionId() { return this.previewSessionId; } getType() { return this.sessionData?.type ?? null; } getAllocations() { return this.currentAllocations; } async refresh() { this.log("Preview mode: refresh is a no-op"); } buildIframeUrl() { const url = new URL(this.widgetUrl); url.searchParams.set("previewMode", "true"); if (this.styleMode) { url.searchParams.set("styleMode", this.styleMode); } return url.toString(); } sendInitMessage() { this.initMessagePending = true; if (this.widgetReadyForInit) { this.doSendInitMessage(); } else { this.log("Waiting for widget-ready-for-init signal..."); } } doSendInitMessage() { this.initMessagePending = false; this.log("Sending preview-init message to widget"); this.sendMessageToWidget({ type: "preview-init", config: this.previewConfig }); } // Override setupMessageListener to capture session ID from ready event setupMessageListener() { super.setupMessageListener(); this.on("ready", (data) => { if (data && data.sessionId) { this.previewSessionId = data.sessionId; this.log("Preview session ID set from widget:", this.previewSessionId); } }); } }; export { WidgetError, WidgetErrorCode, WidgetErrors, createPreviewWidget, createWidget }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map