UNPKG

@tiun/sdk

Version:

tiun SDK for payments and subscriptions

445 lines (444 loc) 14.8 kB
var R = Object.defineProperty; var v = (i, e, t) => e in i ? R(i, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : i[e] = t; var o = (i, e, t) => (v(i, typeof e != "symbol" ? e + "" : e, t), t); const f = ["en", "de", "fr"], p = /* @__PURE__ */ new Set(); function w(i) { if (i == null || i === "") return "en"; const e = i.trim().toLowerCase(); return f.includes(e) ? e : (p.has(e) || (p.add(e), console.warn( `[tiun] Unsupported language "${i}" passed to tiun.init(). Falling back to "en". Supported languages: ${f.join(", ")}.` )), "en"); } const y = ["formal", "informal"], T = /* @__PURE__ */ new Set(); function S(i) { if (i == null || i === "") return; const e = i.trim().toLowerCase(); if (y.includes(e)) return e; T.has(e) || (T.add(e), console.warn( `[tiun] Unsupported tone "${i}" passed to tiun.init(). Falling back to the default ("formal"). Supported tones: ${y.join(", ")}.` )); } const U = "https://api.tiun.live/live_api", C = "https://api-sandbox.tiun.live/live_api"; function m(i, e) { return e || (i ? C : U); } function N(i) { typeof window < "u" && (window.__TIUN_CONFIG__ = i); } function b() { typeof window < "u" && delete window.__TIUN_CONFIG__; } class k { constructor() { o(this, "config", {}); o(this, "initialized", !1); o(this, "ready", !1); o(this, "readyPromise", null); o(this, "readyResolve", null); o(this, "eventListeners", /* @__PURE__ */ new Map()); o(this, "boundMessageHandler", null); // Auth state (populated by TIUN_USER_CHANGE events from the snippet) o(this, "_isAuthenticated", !1); o(this, "_user", null); // =========================================================================== // Internal Methods // =========================================================================== o(this, "pendingTokenResolve", null); o(this, "pendingTokenTimeout", null); } // =========================================================================== // Initialization // =========================================================================== /** * Initialize the tiun SDK. * Call this once at app startup. * * @example NPM package usage (snippetId required) * ```typescript * tiun.init({ * snippetId: 'your-snippet-id', * language: 'en', * debug: true, * }); * ``` * * @example Legacy script tag usage (no config needed) * ```typescript * tiun.init(); * ``` */ init(e = {}) { return typeof window > "u" ? (this.log("SSR detected, skipping initialization"), this) : this.initialized ? (this.log("Already initialized, updating config"), this.config = { ...this.config, ...e }, this.setRuntimeConfigFromTiunConfig(e), this) : (this.config = e, this.initialized = !0, this.readyPromise = new Promise((t) => { this.readyResolve = t; }), this.boundMessageHandler = this.handleMessage.bind(this), window.addEventListener("message", this.boundMessageHandler), this.setRuntimeConfigFromTiunConfig(e), this.log( "SDK initialized", e.snippetId ? "(NPM mode)" : "(legacy mode)" ), e.snippetId && window.dispatchEvent( new CustomEvent("tiun:config-ready", { detail: e }) ), this); } /** * Convert TiunConfig to TiunRuntimeConfig. Only public init fields are set. * corners, webView, preview, previewProductId, enableLogs are never set here – * they are only set via direct snippet URL query params or legacy env. */ setRuntimeConfigFromTiunConfig(e) { const t = e.baseUrl, n = { snippetId: e.snippetId, baseUrl: m(e.sandbox, t), // Normalize (and warn once) here so any language/tone issue surfaces at // init() on the host page, and the snippet receives an already-valid value // instead of re-normalizing and warning a second time in its own console. language: w(e.language), tone: S(e.tone), debug: e.debug }; N(n); } // =========================================================================== // Public Methods // =========================================================================== /** * Open the time-based connect flow. * Use this for publishers with time-based access (no specific product). * * @example * ```typescript * tiun.start(); * ``` */ async start() { this.ensureInitialized(), await this.waitForReady(), this.emit("TIUN_START"), this.log("Start triggered"); } /** * Open the checkout flow for a specific product. * * @example * ```typescript * // Default checkout * tiun.checkout(); * * // With specific product * tiun.checkout({ productId: 'prod_premium_monthly' }); * ``` */ async checkout(e = {}) { this.ensureInitialized(), await this.waitForReady(), this.emit("TIUN_CHECKOUT", { productId: e.productId }), this.log("Checkout triggered", e); } /** * Update the current content context. * Use this to tell tiun what content the user is viewing. * * @example * ```typescript * tiun.setContent({ type: 'premium', contentId: 'article-123' }); * ``` */ async setContent(e) { this.ensureInitialized(), await this.waitForReady(), this.emit("TIUN_UPDATE_CONTENT", { contentId: e.contentId, contentType: e.type, mediaType: e.mediaType || "text" }), this.log("Content updated", e); } /** * Open the OTP login modal for returning subscribers. */ async login() { this.ensureInitialized(), await this.waitForReady(), this.emit("TIUN_LOGIN"), this.log("Login triggered"); } /** * Clear session and fire logout + userChange events. */ logout() { this.ensureInitialized(), this.emit("TIUN_LOGOUT"), this.log("Logout triggered"); } /** * Returns the cached user state. * The snippet keeps this in sync automatically via TIUN_USER_CHANGE events. */ getUser() { return { isAuthenticated: this._isAuthenticated, user: this._user }; } /** * Returns a signed JWT for server-to-server user verification. * Valid for 5 minutes. Returns null if the user is not authenticated. * * The JWT is generated by the snippet (which owns the ECDSA key pair) * via a postMessage round-trip. */ async getUserVerificationToken() { return this._isAuthenticated ? this.requestTokenFromSnippet() : null; } // =========================================================================== // Event System // =========================================================================== /** * Subscribe to SDK events. * Returns an unsubscribe function. * * @example * ```typescript * // Subscribe * const unsubscribe = tiun.on('userChange', (event) => { * console.log('User state changed:', event); * }); * * // Unsubscribe when done * unsubscribe(); * ``` */ on(e, t) { return this.eventListeners.has(e) || this.eventListeners.set(e, /* @__PURE__ */ new Set()), this.eventListeners.get(e).add(t), () => { var n; (n = this.eventListeners.get(e)) == null || n.delete(t); }; } /** * Subscribe to an event for a single invocation. */ once(e, t) { const n = this.on(e, (s) => { n(), t(s); }); return n; } /** * Remove all listeners for an event, or all listeners if no event specified. */ off(e) { e ? this.eventListeners.delete(e) : this.eventListeners.clear(); } // =========================================================================== // Lifecycle // =========================================================================== /** * Wait for the SDK to be ready. * Resolves immediately if already ready. */ async waitForReady() { if (!this.ready) return this.readyPromise || (this.readyPromise = new Promise((e) => { this.readyResolve = e; })), this.readyPromise; } /** * Destroy the SDK and cleanup. */ destroy() { typeof window > "u" || (this.boundMessageHandler && (window.removeEventListener("message", this.boundMessageHandler), this.boundMessageHandler = null), this.eventListeners.clear(), b(), this.handleVerificationTokenResponse(null), this.initialized = !1, this.ready = !1, this.readyPromise = null, this.readyResolve = null, this.config = {}, this._isAuthenticated = !1, this._user = null, this.log("SDK destroyed")); } // =========================================================================== // Static Properties // =========================================================================== /** * SDK version */ get version() { return "0.9.1"; } /** * Whether SDK is initialized */ get isInitialized() { return this.initialized; } /** * Whether SDK is ready (snippet loaded) */ get isReady() { return this.ready; } /** * Whether running in browser */ get isBrowser() { return typeof window < "u"; } /** * Whether the user is identified (has valid session) */ get isAuthenticated() { return this._isAuthenticated; } /** * Current user info or null */ get user() { return this._user; } requestTokenFromSnippet() { return typeof window > "u" ? Promise.resolve(null) : (this.handleVerificationTokenResponse(null), new Promise((e) => { this.pendingTokenResolve = e, this.pendingTokenTimeout = setTimeout(() => { this.pendingTokenResolve = null, e(null); }, 5e3), this.emit("TIUN_GET_VERIFICATION_TOKEN"); })); } handleVerificationTokenResponse(e) { this.pendingTokenResolve && (this.pendingTokenTimeout && (clearTimeout(this.pendingTokenTimeout), this.pendingTokenTimeout = null), this.pendingTokenResolve(e), this.pendingTokenResolve = null); } ensureInitialized() { this.initialized || this.init(); } handleMessage(e) { var n, s, r, a, d, u; if (typeof window < "u" && e.origin !== window.location.origin) return; const t = e.data; if ((t == null ? void 0 : t.type) === "TIUN_EVENT") switch (this.log("Received event", t.content, t), t.content) { case "TIUN_SNIPPET_INITIALIZED": this.handleReady(); break; case "TIUN_SHOW_PAYWALL": this.triggerEvent("paywallShow", { isConnected: t.isConnected ?? !1 }), (s = (n = this.config).onPaywallShow) == null || s.call(n, { isConnected: t.isConnected ?? !1 }); break; case "TIUN_HIDE_PAYWALL": this.triggerEvent("paywallHide", { sessionId: t.sessionId ?? "", isConnected: t.isConnected ?? !0 }), (a = (r = this.config).onPaywallHide) == null || a.call(r, { sessionId: t.sessionId ?? "", isConnected: t.isConnected ?? !0 }); break; case "TIUN_ERROR": t.error && (this.triggerEvent("error", t.error), (u = (d = this.config).onError) == null || u.call(d, t.error)); break; case "TIUN_USER_CHANGE": this.handleUserChange(t); break; case "TIUN_VERIFICATION_TOKEN": this.handleVerificationTokenResponse(t.token ?? null); break; } } handleReady() { var e, t, n; this.ready || (this.ready = !0, (e = this.readyResolve) == null || e.call(this), this.triggerEvent("ready", void 0), (n = (t = this.config).onReady) == null || n.call(t), this.log("Snippet ready")); } triggerEvent(e, t) { const n = this.eventListeners.get(e); n && n.forEach((s) => { try { s(t); } catch (r) { console.error(`[Tiun] Error in ${e} listener:`, r); } }); } handleUserChange(e) { var s, r, a, d, u, c; const t = e.userEvent ?? "update"; this._isAuthenticated = e.isAuthenticated ?? !1, this._user = e.user ?? null; const n = { event: t, isAuthenticated: this._isAuthenticated, user: this._user }; if (this.triggerEvent("userChange", n), (r = (s = this.config).onUserChange) == null || r.call(s, n), t === "login" && this._user) { const g = { user: this._user }; this.triggerEvent("login", g), (d = (a = this.config).onLogin) == null || d.call(a, g); } t === "logout" && (this.triggerEvent("logout", void 0), (c = (u = this.config).onLogout) == null || c.call(u)); } emit(e, t = {}) { if (typeof window > "u") { this.log("Cannot emit in SSR"); return; } window.postMessage( { type: "TIUN_EVENT", content: e, ...t }, window.location.origin ); } log(...e) { this.config.debug && console.log("[tiun]", ...e); } } const l = new k(), _ = "script[data-tiun-snippet][data-snippet-id]", I = "link[data-tiun-snippet][data-snippet-id]"; function L(i) { const e = new URLSearchParams(); i.language != null && i.language !== "" && e.set("language", w(i.language)), i.tone === "formal" && e.set("formal", "true"); const t = e.toString(); return t ? `?${t}` : ""; } function A(i, e, t) { const n = i.replace(/\/$/, ""), s = L(t); return { scriptUrl: `${n}/v2/snippets/${e}/background_js${s}`, cssUrl: `${n}/v2/snippets/${e}/background_css${s}` }; } function E(i) { if (typeof document > "u") return !1; const e = document.querySelector( `${_}[data-snippet-id="${i}"]` ), t = document.querySelector( `${I}[data-snippet-id="${i}"]` ); return !!e || !!t; } function P() { return typeof document > "u" ? !1 : !!document.querySelector( `${_}, ${I}` ); } let h = !1; function z(i, e, t) { if (!(typeof document > "u") && !E(e) && !h) { h = !0; try { const { scriptUrl: n, cssUrl: s } = A( i, e, t ), r = document.createElement("link"); r.rel = "stylesheet", r.href = s, r.setAttribute("data-tiun-snippet", ""), r.setAttribute("data-snippet-id", e), document.head.appendChild(r); const a = document.createElement("script"); a.src = n, a.async = !0, a.setAttribute("data-tiun-snippet", ""), a.setAttribute("data-snippet-id", e), document.head.appendChild(a); } finally { h = !1; } } } const O = l.init.bind(l); l.init = function(i = {}) { const e = O(i); if (typeof window < "u") if (i.snippetId) { if (!E(i.snippetId)) { const t = i.baseUrl, n = m(i.sandbox, t); z(n, i.snippetId, { language: i.language, tone: i.tone }); } } else P() || console.warn( `[tiun] No "snippetId" was provided to tiun.init(), so the snippet cannot load. Pass it as tiun.init({ snippetId: 'your-id' }). (If you use the legacy script-tag integration, you can ignore this.)` ); return e; }; typeof window < "u" && (window.tiun = l); export { l as default, l as tiun };