UNPKG

@habit.analytics/habit-smartlink-reactcomponent

Version:

A React component for Habit SmartLink integration.

248 lines (212 loc) 8.96 kB
/** * smartlink-script.js * * Framework-agnostic Smartlink embed — drop this file in your project and * include it with a <script> tag. Exposes window.createSmartlink globally. * * No build step, no npm install, no framework required. */ (function (global) { "use strict"; /*──────────────────────────── CONSTANTS ────────────────────────────*/ var SMARTLINK_BASE_URL = { int: "https://distributors.integrations.habit.io/habit-smart-link/", qa: "https://distributors.qa.habit.io/habit-smart-link/", default: "https://distributors.habit.io/habit-smart-link/", }; var STRUCTURED_ANNOUNCEMENT = "SMARTLINK_STRUCTURED"; var ANNOUNCEMENT_INTERVAL_MS = 500; var ANNOUNCEMENT_MAX_ATTEMPTS = 10; /*─────────────────────────── HELPERS ───────────────────────────────*/ function detectEnv(href) { if (href.indexOf("localhost") !== -1) return "int"; if (href.indexOf("integrations") !== -1) return "int"; if (href.indexOf("qa") !== -1) return "qa"; return "default"; } function assembleURL(baseURL, hash, pin) { return baseURL + "?hash=" + hash + (pin ? "&pin=" + pin : ""); } function buildMessage(type, payload, requestId) { var msg = { type: type }; if (payload !== undefined) msg.payload = payload; if (requestId !== undefined) msg.requestId = requestId; return msg; } /*──────────────────────────── FACTORY ──────────────────────────────*/ /** * createSmartlink(options) → { destroy } * * options: * container {HTMLElement} Where the iframe is appended. Required. * hash {string} Smartlink hash. Required. * pin {string} Smartlink pin. Optional. * env {string} 'localhost' | 'int' | 'qa' | 'default'. * Auto-detected from window.location if omitted. * prePaymentMethod {Function} async (quoteId) => { success, payment_id }. Required. * onPaymentSuccess {Function} (paymentData) => void. Optional. * onCancelled {Function} () => void. Optional. * onError {Function} ({ message, details }) => void. Optional. * customStyle {Object} Key/value pairs applied to iframe.style. Optional. */ function createSmartlink(options) { var container = options.container; var hash = options.hash; var pin = options.pin; var env = options.env || detectEnv(window.location.href); var prePaymentMethod = options.prePaymentMethod; var onPaymentSuccess = options.onPaymentSuccess || function () {}; var onCancelled = options.onCancelled || function () {}; var onError = options.onError || function () {}; var customStyle = options.customStyle || {}; if (!container) throw new Error("[Smartlink] options.container is required."); if (!hash) throw new Error("[Smartlink] options.hash is required."); if (typeof prePaymentMethod !== "function") throw new Error( "[Smartlink] options.prePaymentMethod must be a function.", ); var baseUrl = SMARTLINK_BASE_URL[env] || SMARTLINK_BASE_URL["default"]; var src = assembleURL(baseUrl, hash, pin); var pluginOrigin = new URL(baseUrl).origin; /*── Create & mount iframe ──*/ var iframe = document.createElement("iframe"); iframe.id = "paymentIframe"; iframe.src = src; iframe.title = "smart component"; iframe.setAttribute("scrolling", "no"); // Default dimensions and styles iframe.style.width = "320px"; iframe.style.height = "650px"; iframe.style.overflow = "hidden"; iframe.style.display = "block"; iframe.style.border = "none"; // Apply any caller overrides. Object.keys(customStyle).forEach(function (prop) { iframe.style[prop] = customStyle[prop]; }); container.appendChild(iframe); function send(msg) { if (iframe.contentWindow) { iframe.contentWindow.postMessage(msg, pluginOrigin); } } function sendInit(requestId) { send(buildMessage("SMARTLINK_INIT", undefined, requestId)); } function handleMessage(event) { if (event.origin !== pluginOrigin) return; var msg = event.data; // Ignore the plain-string legacy announcement echoes from the child. if (!msg || typeof msg !== "object" || !msg.type) return; switch (msg.type) { /* * Child is mounted and ready — send SMARTLINK_INIT to start the flow. * Mirrors: subscribeTo('SMARTLINK_READY', ...) in useSmartlinkComponent. */ case "SMARTLINK_READY": sendInit(msg.requestId); break; /* * Child requests the iframe to be resized. * Mirrors: SMARTLINK_RESIZE handler in useSmartlinkParentMessaging. */ case "SMARTLINK_RESIZE": var resize = msg.payload || {}; if (typeof resize.height === "number") { iframe.style.height = resize.height + "px"; } if (typeof resize.width === "number") { iframe.style.width = resize.width + "px"; } break; /* * Child needs a payment_id before proceeding. * Mirrors: subscribeTo('SMARTLINK_PREPAYMENT_METHOD', ...) in useSmartlinkComponent. */ case "SMARTLINK_PREPAYMENT_METHOD": var requestId = msg.requestId; var quoteId = (msg.payload && msg.payload.quote_id) || ""; Promise.resolve() .then(function () { return prePaymentMethod(quoteId); }) .then(function (result) { send( buildMessage( "SMARTLINK_PREPAYMENT_METHOD_COMPLETE", { success: result.success, payment_id: result.payment_id }, requestId, ), ); }) .catch(function (err) { send( buildMessage( "SMARTLINK_PREPAYMENT_METHOD_COMPLETE", { success: false, payment_id: "", error: err instanceof Error ? err.message : String(err), }, requestId, ), ); }); break; /* * A step completed — if it was the payments step, fire onPaymentSuccess. * Mirrors: subscribeTo('SMARTLINK_STEP_COMPLETE', ...) in useSmartlinkComponent. */ case "SMARTLINK_STEP_COMPLETE": var stepPayload = msg.payload || {}; if (stepPayload.success && stepPayload.source === "payments") { onPaymentSuccess(stepPayload.payment_data || JSON.stringify({})); } break; case "SMARTLINK_CANCELLED": onCancelled(); break; case "SMARTLINK_ERROR": onError(msg.payload || {}); break; } } window.addEventListener("message", handleMessage); /* * Legacy structured announcement — mirrors the announcement useEffect. * Tells older iframe builds that this parent speaks the structured protocol. * Can be removed once all iframe builds respond to SMARTLINK_READY. */ var announcementAttempts = 0; var announcementInterval = setInterval(function () { announcementAttempts++; if (iframe.contentWindow) { iframe.contentWindow.postMessage(STRUCTURED_ANNOUNCEMENT, pluginOrigin); } if (announcementAttempts >= ANNOUNCEMENT_MAX_ATTEMPTS) { clearInterval(announcementInterval); } }, ANNOUNCEMENT_INTERVAL_MS); // Fire once immediately (interval waits one tick before first call). if (iframe.contentWindow) { iframe.contentWindow.postMessage(STRUCTURED_ANNOUNCEMENT, pluginOrigin); } /*── Public API ──*/ return { /** * Remove the iframe from the DOM and clean up all event listeners. * Call this when navigating away or when the component is no longer needed. */ destroy: function () { clearInterval(announcementInterval); window.removeEventListener("message", handleMessage); if (iframe.parentNode) { iframe.parentNode.removeChild(iframe); } }, }; } /*──────────────────────── GLOBAL EXPORT ────────────────────────────*/ global.createSmartlink = createSmartlink; })(typeof window !== "undefined" ? window : this);