@tiun/sdk
Version:
tiun SDK for payments and subscriptions
445 lines (444 loc) • 14.8 kB
JavaScript
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
};