@dollardonationclub/impactapi-js
Version:
JavaScript SDK for integrating the DDC Impact Widget into any website
495 lines (489 loc) • 16.5 kB
JavaScript
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