UNPKG

@tiun/sdk

Version:

tiun SDK for payments and subscriptions

478 lines (477 loc) • 15.9 kB
var U = Object.defineProperty; var C = (n, e, t) => e in n ? U(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t; var o = (n, e, t) => (C(n, typeof e != "symbol" ? e + "" : e, t), t); const g = ["en", "de", "fr"], f = /* @__PURE__ */ new Set(); function I(n) { if (n == null || n === "") return "en"; const e = n.trim().toLowerCase(); return g.includes(e) ? e : (f.has(e) || (f.add(e), console.warn( `[tiun] Unsupported language "${n}" passed to tiun.init(). Falling back to "en". Supported languages: ${g.join(", ")}.` )), "en"); } const w = ["formal", "informal"], T = /* @__PURE__ */ new Set(); function A(n) { if (n == null || n === "") return; const e = n.trim().toLowerCase(); if (w.includes(e)) return e; T.has(e) || (T.add(e), console.warn( `[tiun] Unsupported tone "${n}" passed to tiun.init(). Falling back to the snippet default ("informal"). Supported tones: ${w.join(", ")}.` )); } const y = ["light", "dark", "auto"], m = /* @__PURE__ */ new Set(); function _(n) { if (n == null || n === "") return; const e = n.trim().toLowerCase(); if (y.includes(e)) return e; m.has(e) || (m.add(e), console.warn( `[tiun] Unsupported appearance "${n}". Supported appearances: ${y.join(", ")}.` )); } const N = "https://api.tiun.live/live_api", P = "https://api-sandbox.tiun.live/live_api"; function E(n, e) { return e || (n ? P : N); } function k(n) { typeof window < "u" && (window.__TIUN_CONFIG__ = n); } function b() { typeof window < "u" && delete window.__TIUN_CONFIG__; } class L { 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), e.appearance !== void 0 && this.setAppearance(e.appearance), this) : (this.config = { ...e, appearance: e.appearance ?? this.config.appearance }, e = this.config, 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. * 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, i = { snippetId: e.snippetId, baseUrl: E(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: I(e.language), tone: A(e.tone), appearance: _(e.appearance), debug: e.debug }; k(i); } // =========================================================================== // 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); } /** * Switch the snippet's color theme at runtime. * Safe to call before init() or before the snippet is ready: a snippet that * hasn't booted picks the value up from the runtime config, a running one * from the event. * * @example * ```typescript * tiun.setAppearance('dark'); * ``` */ setAppearance(e) { const t = _(e); if (!t || (this.config.appearance = t, !this.initialized || typeof window > "u")) return; const i = window.__TIUN_CONFIG__; i && (i.appearance = t), this.emit("TIUN_SET_APPEARANCE", { appearance: t }), this.log("Appearance set", t); } /** * 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 i; (i = this.eventListeners.get(e)) == null || i.delete(t); }; } /** * Subscribe to an event for a single invocation. */ once(e, t) { const i = this.on(e, (s) => { i(), t(s); }); return i; } /** * 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.10.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 i, 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 = (i = this.config).onPaywallShow) == null || s.call(i, { 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, i; this.ready || (this.ready = !0, (e = this.readyResolve) == null || e.call(this), this.triggerEvent("ready", void 0), (i = (t = this.config).onReady) == null || i.call(t), this.log("Snippet ready")); } triggerEvent(e, t) { const i = this.eventListeners.get(e); i && i.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 i = { event: t, isAuthenticated: this._isAuthenticated, user: this._user }; if (this.triggerEvent("userChange", i), (r = (s = this.config).onUserChange) == null || r.call(s, i), t === "login" && this._user) { const p = { user: this._user }; this.triggerEvent("login", p), (d = (a = this.config).onLogin) == null || d.call(a, p); } 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 L(), R = "script[data-tiun-snippet][data-snippet-id]", S = "link[data-tiun-snippet][data-snippet-id]"; function z(n) { const e = new URLSearchParams(); n.language != null && n.language !== "" && e.set("language", I(n.language)), n.tone === "formal" && e.set("formal", "true"); const t = e.toString(); return t ? `?${t}` : ""; } function O(n, e, t) { const i = n.replace(/\/$/, ""), s = z(t); return { scriptUrl: `${i}/v2/snippets/${e}/background_js${s}`, cssUrl: `${i}/v2/snippets/${e}/background_css${s}` }; } function v(n) { if (typeof document > "u") return !1; const e = document.querySelector( `${R}[data-snippet-id="${n}"]` ), t = document.querySelector( `${S}[data-snippet-id="${n}"]` ); return !!e || !!t; } function $() { return typeof document > "u" ? !1 : !!document.querySelector( `${R}, ${S}` ); } let h = !1; function F(n, e, t) { if (!(typeof document > "u") && !v(e) && !h) { h = !0; try { const { scriptUrl: i, cssUrl: s } = O( n, 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 = i, a.async = !0, a.setAttribute("data-tiun-snippet", ""), a.setAttribute("data-snippet-id", e), document.head.appendChild(a); } finally { h = !1; } } } const H = l.init.bind(l); l.init = function(n = {}) { const e = H(n); if (typeof window < "u") if (n.snippetId) { if (!v(n.snippetId)) { const t = n.baseUrl, i = E(n.sandbox, t); F(i, n.snippetId, { language: n.language, tone: n.tone }); } } else $() || 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 };