@tolinku/web-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/index.d.mts +298 -0
- package/dist/index.d.ts +298 -0
- package/dist/index.js +913 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +910 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var MAX_RETRIES = 3;
|
|
3
|
+
var BASE_DELAY_MS = 500;
|
|
4
|
+
var MAX_JITTER_MS = 250;
|
|
5
|
+
var HttpClient = class {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.abortController = null;
|
|
8
|
+
this._baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
9
|
+
this.apiKey = config.apiKey;
|
|
10
|
+
}
|
|
11
|
+
/** Abort all in-flight requests (called by Tolinku.destroy()) */
|
|
12
|
+
abort() {
|
|
13
|
+
if (this.abortController) {
|
|
14
|
+
this.abortController.abort();
|
|
15
|
+
this.abortController = null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Get a signal for the current request batch, creating a controller if needed */
|
|
19
|
+
get signal() {
|
|
20
|
+
if (!this.abortController) {
|
|
21
|
+
this.abortController = new AbortController();
|
|
22
|
+
}
|
|
23
|
+
return this.abortController.signal;
|
|
24
|
+
}
|
|
25
|
+
/** Public accessor for the base URL */
|
|
26
|
+
get baseUrl() {
|
|
27
|
+
return this._baseUrl;
|
|
28
|
+
}
|
|
29
|
+
/** Public accessor for the API key (used by sendBeacon which cannot set custom headers) */
|
|
30
|
+
get key() {
|
|
31
|
+
return this.apiKey;
|
|
32
|
+
}
|
|
33
|
+
async get(path, params) {
|
|
34
|
+
let url = this._baseUrl + path;
|
|
35
|
+
if (params) {
|
|
36
|
+
const qs = new URLSearchParams(params).toString();
|
|
37
|
+
if (qs) url += "?" + qs;
|
|
38
|
+
}
|
|
39
|
+
const res = await this.fetchWithRetry(url, {
|
|
40
|
+
method: "GET",
|
|
41
|
+
headers: this.headers(),
|
|
42
|
+
signal: this.signal
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
46
|
+
throw new TolinkuError(body.error || res.statusText, res.status, body.code);
|
|
47
|
+
}
|
|
48
|
+
return this.parseJson(res);
|
|
49
|
+
}
|
|
50
|
+
async post(path, body) {
|
|
51
|
+
const res = await this.fetchWithRetry(this._baseUrl + path, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
...this.headers(),
|
|
55
|
+
"Content-Type": "application/json"
|
|
56
|
+
},
|
|
57
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
58
|
+
signal: this.signal
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const data = await res.json().catch(() => ({ error: res.statusText }));
|
|
62
|
+
throw new TolinkuError(data.error || res.statusText, res.status, data.code);
|
|
63
|
+
}
|
|
64
|
+
return this.parseJson(res);
|
|
65
|
+
}
|
|
66
|
+
/** GET without API key auth (for public endpoints like banner config) */
|
|
67
|
+
async getPublic(path, params) {
|
|
68
|
+
let url = this._baseUrl + path;
|
|
69
|
+
if (params) {
|
|
70
|
+
const qs = new URLSearchParams(params).toString();
|
|
71
|
+
if (qs) url += "?" + qs;
|
|
72
|
+
}
|
|
73
|
+
const res = await this.fetchWithRetry(url, {
|
|
74
|
+
method: "GET",
|
|
75
|
+
signal: this.signal
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
79
|
+
throw new TolinkuError(body.error || res.statusText, res.status, body.code);
|
|
80
|
+
}
|
|
81
|
+
return this.parseJson(res);
|
|
82
|
+
}
|
|
83
|
+
/** POST without API key auth (for public endpoints like deferred claim) */
|
|
84
|
+
async postPublic(path, body) {
|
|
85
|
+
const res = await this.fetchWithRetry(this._baseUrl + path, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "Content-Type": "application/json" },
|
|
88
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
89
|
+
signal: this.signal
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const data = await res.json().catch(() => ({ error: res.statusText }));
|
|
93
|
+
throw new TolinkuError(data.error || res.statusText, res.status, data.code);
|
|
94
|
+
}
|
|
95
|
+
return this.parseJson(res);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Fetch with retry logic. Retries on network errors, HTTP 429, and 5xx responses.
|
|
99
|
+
* Uses exponential backoff: BASE_DELAY_MS * 2^attempt + random jitter.
|
|
100
|
+
* Respects Retry-After header on 429 responses.
|
|
101
|
+
* Does NOT retry on 4xx errors (except 429) or successful responses.
|
|
102
|
+
*/
|
|
103
|
+
async fetchWithRetry(url, init) {
|
|
104
|
+
let lastError;
|
|
105
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(url, init);
|
|
108
|
+
if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) {
|
|
109
|
+
return res;
|
|
110
|
+
}
|
|
111
|
+
if (attempt < MAX_RETRIES) {
|
|
112
|
+
const delay = this.computeDelay(attempt, res);
|
|
113
|
+
await this.sleep(delay, init.signal);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
return res;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
lastError = err;
|
|
122
|
+
if (attempt < MAX_RETRIES) {
|
|
123
|
+
const delay = this.computeDelay(attempt);
|
|
124
|
+
await this.sleep(delay, init.signal);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw lastError;
|
|
130
|
+
}
|
|
131
|
+
/** Compute backoff delay: BASE_DELAY_MS * 2^attempt + random jitter (0 to MAX_JITTER_MS) */
|
|
132
|
+
computeDelay(attempt, res) {
|
|
133
|
+
if (res && res.status === 429) {
|
|
134
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
135
|
+
if (retryAfter) {
|
|
136
|
+
const seconds = Number(retryAfter);
|
|
137
|
+
if (!isNaN(seconds) && seconds > 0) {
|
|
138
|
+
return seconds * 1e3;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const exponential = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
143
|
+
const jitter = Math.random() * MAX_JITTER_MS;
|
|
144
|
+
return exponential + jitter;
|
|
145
|
+
}
|
|
146
|
+
/** Sleep for a given duration, but throw if the signal is aborted */
|
|
147
|
+
sleep(ms, signal) {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
if (signal?.aborted) {
|
|
150
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const timer = setTimeout(resolve, ms);
|
|
154
|
+
signal?.addEventListener("abort", () => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
157
|
+
}, { once: true });
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Safely parse JSON from a response, handling non-JSON 200s */
|
|
161
|
+
async parseJson(res) {
|
|
162
|
+
try {
|
|
163
|
+
return await res.json();
|
|
164
|
+
} catch {
|
|
165
|
+
throw new TolinkuError("Invalid JSON in response body", res.status);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
headers() {
|
|
169
|
+
return {
|
|
170
|
+
"X-API-Key": this.apiKey
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var TolinkuError = class extends Error {
|
|
175
|
+
constructor(message, status, code) {
|
|
176
|
+
super(message);
|
|
177
|
+
this.name = "TolinkuError";
|
|
178
|
+
this.status = status;
|
|
179
|
+
this.code = code;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/analytics.ts
|
|
184
|
+
var BATCH_SIZE = 10;
|
|
185
|
+
var FLUSH_INTERVAL_MS = 5e3;
|
|
186
|
+
var MAX_QUEUE_SIZE = 1e3;
|
|
187
|
+
var Analytics = class {
|
|
188
|
+
constructor(client) {
|
|
189
|
+
this.client = client;
|
|
190
|
+
this.queue = [];
|
|
191
|
+
this.flushTimer = null;
|
|
192
|
+
this.unloadHandler = null;
|
|
193
|
+
if (typeof window !== "undefined") {
|
|
194
|
+
this.unloadHandler = () => this.flushBeacon();
|
|
195
|
+
window.addEventListener("beforeunload", this.unloadHandler);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Track a custom event. Event type must start with "custom." and match
|
|
200
|
+
* the pattern custom.[a-z0-9_]+
|
|
201
|
+
*
|
|
202
|
+
* Events are queued and sent in batches for efficiency.
|
|
203
|
+
*/
|
|
204
|
+
async track(eventType, properties) {
|
|
205
|
+
if (typeof eventType !== "string" || eventType.trim().length === 0) {
|
|
206
|
+
throw new Error("Tolinku: event name must be a non-empty string");
|
|
207
|
+
}
|
|
208
|
+
if (!eventType.startsWith("custom.")) {
|
|
209
|
+
eventType = "custom." + eventType;
|
|
210
|
+
}
|
|
211
|
+
const eventNameRegex = /^custom\.[a-z0-9_]+$/;
|
|
212
|
+
if (!eventNameRegex.test(eventType)) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Tolinku: event name "${eventType}" is invalid. Event names must match the pattern "custom.[a-z0-9_]+" (lowercase letters, numbers, and underscores only after "custom.")`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
this.queue.push({
|
|
218
|
+
event_type: eventType,
|
|
219
|
+
properties: properties || {}
|
|
220
|
+
});
|
|
221
|
+
if (this.queue.length === 1 && !this.flushTimer) {
|
|
222
|
+
this.flushTimer = setTimeout(() => {
|
|
223
|
+
this.flushTimer = null;
|
|
224
|
+
this.flush();
|
|
225
|
+
}, FLUSH_INTERVAL_MS);
|
|
226
|
+
}
|
|
227
|
+
if (this.queue.length >= BATCH_SIZE) {
|
|
228
|
+
await this.flush();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Send all queued events to the server */
|
|
232
|
+
async flush() {
|
|
233
|
+
if (this.flushTimer) {
|
|
234
|
+
clearTimeout(this.flushTimer);
|
|
235
|
+
this.flushTimer = null;
|
|
236
|
+
}
|
|
237
|
+
if (this.queue.length === 0) return;
|
|
238
|
+
const events = this.queue.splice(0);
|
|
239
|
+
try {
|
|
240
|
+
const result = await this.client.post("/v1/api/analytics/batch", { events });
|
|
241
|
+
if (result.errors && result.errors.length > 0) {
|
|
242
|
+
console.warn("[TolinkuSDK] Batch partial failure:", result.errors);
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
this.queue.unshift(...events);
|
|
246
|
+
if (this.queue.length > MAX_QUEUE_SIZE) {
|
|
247
|
+
this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Flush remaining events using navigator.sendBeacon (best-effort).
|
|
253
|
+
* Called on page unload when a normal fetch may not complete.
|
|
254
|
+
*/
|
|
255
|
+
flushBeacon() {
|
|
256
|
+
if (this.queue.length === 0) return;
|
|
257
|
+
const events = this.queue.splice(0);
|
|
258
|
+
const url = this.client.baseUrl + "/v1/api/analytics/batch";
|
|
259
|
+
const body = JSON.stringify({ events, apiKey: this.client.key });
|
|
260
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
261
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** Clean up timers and event listeners. Called by Tolinku.destroy(). */
|
|
265
|
+
destroy() {
|
|
266
|
+
this.flushBeacon();
|
|
267
|
+
if (this.flushTimer) {
|
|
268
|
+
clearTimeout(this.flushTimer);
|
|
269
|
+
this.flushTimer = null;
|
|
270
|
+
}
|
|
271
|
+
if (typeof window !== "undefined" && this.unloadHandler) {
|
|
272
|
+
window.removeEventListener("beforeunload", this.unloadHandler);
|
|
273
|
+
this.unloadHandler = null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// src/referrals.ts
|
|
279
|
+
var Referrals = class {
|
|
280
|
+
constructor(client) {
|
|
281
|
+
this.client = client;
|
|
282
|
+
}
|
|
283
|
+
/** Create a new referral for a user */
|
|
284
|
+
async create(options) {
|
|
285
|
+
return this.client.post("/v1/api/referral/create", {
|
|
286
|
+
user_id: options.userId,
|
|
287
|
+
metadata: options.metadata,
|
|
288
|
+
user_name: options.userName
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** Get referral info by code */
|
|
292
|
+
async get(code) {
|
|
293
|
+
return this.client.get(`/v1/api/referral/${encodeURIComponent(code)}`);
|
|
294
|
+
}
|
|
295
|
+
/** Complete a referral (mark as converted) */
|
|
296
|
+
async complete(options) {
|
|
297
|
+
return this.client.post("/v1/api/referral/complete", {
|
|
298
|
+
referral_code: options.code,
|
|
299
|
+
referred_user_id: options.referredUserId,
|
|
300
|
+
milestone: options.milestone,
|
|
301
|
+
referred_user_name: options.referredUserName
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/** Update a referral milestone */
|
|
305
|
+
async milestone(options) {
|
|
306
|
+
return this.client.post("/v1/api/referral/milestone", {
|
|
307
|
+
referral_code: options.code,
|
|
308
|
+
milestone: options.milestone
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/** Claim a referral reward */
|
|
312
|
+
async claimReward(code) {
|
|
313
|
+
return this.client.post("/v1/api/referral/claim-reward", {
|
|
314
|
+
referral_code: code
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/** Get the referral leaderboard */
|
|
318
|
+
async leaderboard(limit = 25) {
|
|
319
|
+
return this.client.get("/v1/api/referral/leaderboard", {
|
|
320
|
+
limit: String(limit)
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/deferred.ts
|
|
326
|
+
var Deferred = class {
|
|
327
|
+
constructor(client) {
|
|
328
|
+
this.client = client;
|
|
329
|
+
}
|
|
330
|
+
/** Claim a deferred deep link by referrer token (from Play Store referrer or clipboard) */
|
|
331
|
+
async claimByToken(token) {
|
|
332
|
+
try {
|
|
333
|
+
return await this.client.getPublic("/v1/api/deferred/claim", { token });
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.warn("[Tolinku] Failed to claim deferred link by token:", err);
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Claim a deferred deep link by device signal matching */
|
|
340
|
+
async claimBySignals(options) {
|
|
341
|
+
try {
|
|
342
|
+
return await this.client.postPublic("/v1/api/deferred/claim-by-signals", {
|
|
343
|
+
appspace_id: options.appspaceId,
|
|
344
|
+
timezone: options.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
345
|
+
language: options.language || navigator.language,
|
|
346
|
+
screen_width: options.screenWidth || window.screen.width,
|
|
347
|
+
screen_height: options.screenHeight || window.screen.height
|
|
348
|
+
});
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.warn("[Tolinku] Failed to claim deferred link by signals:", err);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/storage.ts
|
|
357
|
+
var BANNER_KEY = "tolinku_banner_dismissed";
|
|
358
|
+
var MESSAGE_KEY = "tolinku_message_dismissed";
|
|
359
|
+
var MESSAGE_IMPRESSIONS_KEY = "tolinku_message_impressions";
|
|
360
|
+
var MESSAGE_LAST_SHOWN_KEY = "tolinku_message_last_shown";
|
|
361
|
+
function getStore(key) {
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse(localStorage.getItem(key) || "{}");
|
|
364
|
+
} catch {
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function setStore(key, data) {
|
|
369
|
+
try {
|
|
370
|
+
localStorage.setItem(key, JSON.stringify(data));
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function isBannerDismissed(bannerId, dismissDays) {
|
|
375
|
+
if (!dismissDays || dismissDays <= 0) return false;
|
|
376
|
+
const data = getStore(BANNER_KEY);
|
|
377
|
+
const entry = data[bannerId];
|
|
378
|
+
if (!entry) return false;
|
|
379
|
+
const dismissedAt = new Date(entry).getTime();
|
|
380
|
+
return Date.now() - dismissedAt < dismissDays * 864e5;
|
|
381
|
+
}
|
|
382
|
+
function saveBannerDismissal(bannerId) {
|
|
383
|
+
const data = getStore(BANNER_KEY);
|
|
384
|
+
data[bannerId] = (/* @__PURE__ */ new Date()).toISOString();
|
|
385
|
+
setStore(BANNER_KEY, data);
|
|
386
|
+
}
|
|
387
|
+
function isMessageDismissed(messageId, dismissDays) {
|
|
388
|
+
if (!dismissDays || dismissDays <= 0) return false;
|
|
389
|
+
const data = getStore(MESSAGE_KEY);
|
|
390
|
+
const entry = data[messageId];
|
|
391
|
+
if (!entry) return false;
|
|
392
|
+
const dismissedAt = new Date(entry).getTime();
|
|
393
|
+
return Date.now() - dismissedAt < dismissDays * 864e5;
|
|
394
|
+
}
|
|
395
|
+
function saveMessageDismissal(messageId) {
|
|
396
|
+
const data = getStore(MESSAGE_KEY);
|
|
397
|
+
data[messageId] = (/* @__PURE__ */ new Date()).toISOString();
|
|
398
|
+
setStore(MESSAGE_KEY, data);
|
|
399
|
+
}
|
|
400
|
+
function isMessageSuppressed(messageId, maxImpressions, minIntervalHours) {
|
|
401
|
+
if (maxImpressions !== null && maxImpressions > 0) {
|
|
402
|
+
const impressions = getStore(MESSAGE_IMPRESSIONS_KEY);
|
|
403
|
+
const count = parseInt(impressions[messageId] || "0", 10);
|
|
404
|
+
if (count >= maxImpressions) return true;
|
|
405
|
+
}
|
|
406
|
+
if (minIntervalHours !== null && minIntervalHours > 0) {
|
|
407
|
+
const lastShown = getStore(MESSAGE_LAST_SHOWN_KEY);
|
|
408
|
+
const entry = lastShown[messageId];
|
|
409
|
+
if (entry) {
|
|
410
|
+
const lastShownAt = new Date(entry).getTime();
|
|
411
|
+
const intervalMs = minIntervalHours * 36e5;
|
|
412
|
+
if (Date.now() - lastShownAt < intervalMs) return true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
function recordMessageImpression(messageId) {
|
|
418
|
+
const impressions = getStore(MESSAGE_IMPRESSIONS_KEY);
|
|
419
|
+
const count = parseInt(impressions[messageId] || "0", 10);
|
|
420
|
+
impressions[messageId] = String(count + 1);
|
|
421
|
+
setStore(MESSAGE_IMPRESSIONS_KEY, impressions);
|
|
422
|
+
const lastShown = getStore(MESSAGE_LAST_SHOWN_KEY);
|
|
423
|
+
lastShown[messageId] = (/* @__PURE__ */ new Date()).toISOString();
|
|
424
|
+
setStore(MESSAGE_LAST_SHOWN_KEY, lastShown);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/sanitize.ts
|
|
428
|
+
function sanitizeCssColor(value) {
|
|
429
|
+
if (!value) return null;
|
|
430
|
+
const trimmed = value.trim();
|
|
431
|
+
if (/[;{}]/.test(trimmed)) return null;
|
|
432
|
+
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(trimmed)) return trimmed;
|
|
433
|
+
if (/^(rgb|rgba|hsl|hsla)\([0-9a-zA-Z,.%\s/]+\)$/.test(trimmed)) return trimmed;
|
|
434
|
+
if (/^[a-zA-Z-]{1,30}$/.test(trimmed)) return trimmed;
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/banners.ts
|
|
439
|
+
var Banners = class {
|
|
440
|
+
constructor(client) {
|
|
441
|
+
this.client = client;
|
|
442
|
+
this.container = null;
|
|
443
|
+
this.styleEl = null;
|
|
444
|
+
}
|
|
445
|
+
/** Fetch banner config and show the highest-priority banner */
|
|
446
|
+
async show(options = {}, userId) {
|
|
447
|
+
const params = {};
|
|
448
|
+
if (userId) params.user_id = userId;
|
|
449
|
+
const config = await this.client.getPublic("/v1/api/banner/config", params);
|
|
450
|
+
if (!config.enabled || !config.banners || config.banners.length === 0) return;
|
|
451
|
+
config.banners.sort((a, b) => b.priority - a.priority);
|
|
452
|
+
let banner = null;
|
|
453
|
+
for (const b of config.banners) {
|
|
454
|
+
if (options.label && b.label !== options.label) continue;
|
|
455
|
+
if (!isBannerDismissed(b.id, b.dismiss_days)) {
|
|
456
|
+
banner = b;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!banner) return;
|
|
461
|
+
this.render(config, banner, options);
|
|
462
|
+
}
|
|
463
|
+
/** Remove the banner from the DOM */
|
|
464
|
+
dismiss() {
|
|
465
|
+
if (this.container) {
|
|
466
|
+
this.container.classList.remove("tolk-visible");
|
|
467
|
+
const pos = this.container.dataset.position || "top";
|
|
468
|
+
document.body.style.removeProperty(pos === "top" ? "padding-top" : "padding-bottom");
|
|
469
|
+
setTimeout(() => {
|
|
470
|
+
this.container?.remove();
|
|
471
|
+
this.styleEl?.remove();
|
|
472
|
+
this.container = null;
|
|
473
|
+
this.styleEl = null;
|
|
474
|
+
}, 400);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
render(config, banner, options) {
|
|
478
|
+
if (this.container) {
|
|
479
|
+
this.container.remove();
|
|
480
|
+
this.styleEl?.remove();
|
|
481
|
+
}
|
|
482
|
+
const position = options.position || banner.position || "top";
|
|
483
|
+
const bgColor = sanitizeCssColor(banner.background_color) || "#ffffff";
|
|
484
|
+
const textColor = sanitizeCssColor(banner.text_color) || "#000000";
|
|
485
|
+
const ctaText = banner.cta_text || "Open";
|
|
486
|
+
const baseUrl = this.client.baseUrl;
|
|
487
|
+
const installUrl = banner.action_url || baseUrl + (config.install_url || "/install");
|
|
488
|
+
const safeTop = position === "top" ? "padding-top: env(safe-area-inset-top, 0px);" : "";
|
|
489
|
+
const safeBottom = position === "bottom" ? "padding-bottom: env(safe-area-inset-bottom, 0px);" : "";
|
|
490
|
+
const container = document.createElement("div");
|
|
491
|
+
container.id = "tolinku-banner";
|
|
492
|
+
container.setAttribute("role", "banner");
|
|
493
|
+
container.setAttribute("aria-live", "polite");
|
|
494
|
+
container.dataset.position = position;
|
|
495
|
+
const style = document.createElement("style");
|
|
496
|
+
style.textContent = `
|
|
497
|
+
#tolinku-banner {
|
|
498
|
+
position: fixed;
|
|
499
|
+
${position}: 0;
|
|
500
|
+
left: 0; right: 0;
|
|
501
|
+
z-index: 999999;
|
|
502
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
503
|
+
transform: translateY(${position === "top" ? "-100%" : "100%"});
|
|
504
|
+
transition: transform 0.35s ease;
|
|
505
|
+
${safeTop}${safeBottom}
|
|
506
|
+
}
|
|
507
|
+
#tolinku-banner.tolk-visible { transform: translateY(0); }
|
|
508
|
+
#tolinku-banner .tolk-inner {
|
|
509
|
+
display: flex; align-items: center; gap: 10px;
|
|
510
|
+
padding: 10px 14px;
|
|
511
|
+
background: ${bgColor}; color: ${textColor};
|
|
512
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
513
|
+
}
|
|
514
|
+
#tolinku-banner .tolk-close {
|
|
515
|
+
background: none; border: none; font-size: 20px; line-height: 1;
|
|
516
|
+
cursor: pointer; color: ${textColor}; opacity: 0.6; padding: 0 4px; flex-shrink: 0;
|
|
517
|
+
}
|
|
518
|
+
#tolinku-banner .tolk-close:hover { opacity: 1; }
|
|
519
|
+
#tolinku-banner .tolk-icon {
|
|
520
|
+
width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0; object-fit: cover;
|
|
521
|
+
}
|
|
522
|
+
#tolinku-banner .tolk-text { flex: 1; min-width: 0; }
|
|
523
|
+
#tolinku-banner .tolk-title {
|
|
524
|
+
font-size: 14px; font-weight: 600; margin: 0;
|
|
525
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
526
|
+
}
|
|
527
|
+
#tolinku-banner .tolk-body {
|
|
528
|
+
font-size: 12px; margin: 0; opacity: 0.75;
|
|
529
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
530
|
+
}
|
|
531
|
+
#tolinku-banner .tolk-cta {
|
|
532
|
+
display: inline-block; padding: 6px 16px; border-radius: 100px;
|
|
533
|
+
font-size: 13px; font-weight: 600; text-decoration: none;
|
|
534
|
+
background: ${textColor}; color: ${bgColor}; flex-shrink: 0; text-align: center;
|
|
535
|
+
}
|
|
536
|
+
`;
|
|
537
|
+
const inner = document.createElement("div");
|
|
538
|
+
inner.className = "tolk-inner";
|
|
539
|
+
const closeBtn = document.createElement("button");
|
|
540
|
+
closeBtn.className = "tolk-close";
|
|
541
|
+
closeBtn.setAttribute("aria-label", "Dismiss banner");
|
|
542
|
+
closeBtn.textContent = "\xD7";
|
|
543
|
+
closeBtn.addEventListener("click", () => {
|
|
544
|
+
saveBannerDismissal(banner.id);
|
|
545
|
+
this.dismiss();
|
|
546
|
+
});
|
|
547
|
+
inner.appendChild(closeBtn);
|
|
548
|
+
if (config.app_icon && isSafeUrl(config.app_icon)) {
|
|
549
|
+
const icon = document.createElement("img");
|
|
550
|
+
icon.className = "tolk-icon";
|
|
551
|
+
icon.src = config.app_icon;
|
|
552
|
+
icon.alt = config.app_name || "App";
|
|
553
|
+
inner.appendChild(icon);
|
|
554
|
+
}
|
|
555
|
+
const textWrap = document.createElement("div");
|
|
556
|
+
textWrap.className = "tolk-text";
|
|
557
|
+
const titleEl = document.createElement("p");
|
|
558
|
+
titleEl.className = "tolk-title";
|
|
559
|
+
titleEl.textContent = banner.title || config.app_name || "Get the App";
|
|
560
|
+
textWrap.appendChild(titleEl);
|
|
561
|
+
if (banner.body) {
|
|
562
|
+
const bodyEl = document.createElement("p");
|
|
563
|
+
bodyEl.className = "tolk-body";
|
|
564
|
+
bodyEl.textContent = banner.body;
|
|
565
|
+
textWrap.appendChild(bodyEl);
|
|
566
|
+
}
|
|
567
|
+
inner.appendChild(textWrap);
|
|
568
|
+
const cta = document.createElement("a");
|
|
569
|
+
cta.className = "tolk-cta";
|
|
570
|
+
cta.href = isSafeUrl(installUrl) ? installUrl : "#";
|
|
571
|
+
cta.textContent = ctaText;
|
|
572
|
+
inner.appendChild(cta);
|
|
573
|
+
container.appendChild(inner);
|
|
574
|
+
document.head.appendChild(style);
|
|
575
|
+
document.body.appendChild(container);
|
|
576
|
+
this.container = container;
|
|
577
|
+
this.styleEl = style;
|
|
578
|
+
requestAnimationFrame(() => {
|
|
579
|
+
requestAnimationFrame(() => {
|
|
580
|
+
container.classList.add("tolk-visible");
|
|
581
|
+
const bannerHeight = container.offsetHeight + "px";
|
|
582
|
+
if (position === "top") {
|
|
583
|
+
document.body.style.paddingTop = bannerHeight;
|
|
584
|
+
} else {
|
|
585
|
+
document.body.style.paddingBottom = bannerHeight;
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
function isSafeUrl(url) {
|
|
592
|
+
try {
|
|
593
|
+
const parsed = new URL(url, window.location.href);
|
|
594
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
595
|
+
} catch {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/messages.ts
|
|
601
|
+
var Messages = class {
|
|
602
|
+
constructor(client) {
|
|
603
|
+
this.client = client;
|
|
604
|
+
this.overlay = null;
|
|
605
|
+
this.styleEl = null;
|
|
606
|
+
}
|
|
607
|
+
/** Fetch messages and show the highest-priority non-dismissed one */
|
|
608
|
+
async show(options = {}, userId) {
|
|
609
|
+
const params = {};
|
|
610
|
+
if (options.trigger) params.trigger = options.trigger;
|
|
611
|
+
if (userId) params.user_id = userId;
|
|
612
|
+
const data = await this.client.get("/v1/api/messages", params);
|
|
613
|
+
if (!data.messages || data.messages.length === 0) return;
|
|
614
|
+
const candidates = data.messages.filter((m) => !isMessageDismissed(m.id, m.dismiss_days)).filter((m) => !isMessageSuppressed(m.id, m.max_impressions, m.min_interval_hours)).filter((m) => !options.triggerValue || m.trigger_value === options.triggerValue).sort((a, b) => b.priority - a.priority);
|
|
615
|
+
if (candidates.length === 0) return;
|
|
616
|
+
const message = candidates[0];
|
|
617
|
+
recordMessageImpression(message.id);
|
|
618
|
+
this.render(message, options);
|
|
619
|
+
}
|
|
620
|
+
/** Remove the message overlay */
|
|
621
|
+
dismiss() {
|
|
622
|
+
if (this.overlay) {
|
|
623
|
+
this.overlay.style.opacity = "0";
|
|
624
|
+
setTimeout(() => {
|
|
625
|
+
this.overlay?.remove();
|
|
626
|
+
this.styleEl?.remove();
|
|
627
|
+
this.overlay = null;
|
|
628
|
+
this.styleEl = null;
|
|
629
|
+
}, 300);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
render(message, options) {
|
|
633
|
+
if (this.overlay) {
|
|
634
|
+
this.overlay.remove();
|
|
635
|
+
this.styleEl?.remove();
|
|
636
|
+
}
|
|
637
|
+
const style = document.createElement("style");
|
|
638
|
+
style.textContent = `
|
|
639
|
+
.tolk-msg-overlay {
|
|
640
|
+
position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1000000;
|
|
641
|
+
display: flex; align-items: center; justify-content: center;
|
|
642
|
+
background: rgba(0,0,0,0.5);
|
|
643
|
+
opacity: 0; transition: opacity 0.3s ease;
|
|
644
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
645
|
+
}
|
|
646
|
+
.tolk-msg-overlay.tolk-visible { opacity: 1; }
|
|
647
|
+
.tolk-msg-card {
|
|
648
|
+
position: relative; max-width: 375px; width: 90%;
|
|
649
|
+
max-height: 80vh; overflow-y: auto;
|
|
650
|
+
border-radius: 16px; padding: 24px;
|
|
651
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
652
|
+
}
|
|
653
|
+
.tolk-msg-close {
|
|
654
|
+
position: absolute; top: 12px; right: 12px;
|
|
655
|
+
background: rgba(0,0,0,0.1); border: none; border-radius: 50%;
|
|
656
|
+
width: 28px; height: 28px; font-size: 16px; line-height: 1;
|
|
657
|
+
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
658
|
+
color: inherit; opacity: 0.6;
|
|
659
|
+
}
|
|
660
|
+
.tolk-msg-close:hover { opacity: 1; }
|
|
661
|
+
`;
|
|
662
|
+
const overlay = document.createElement("div");
|
|
663
|
+
overlay.className = "tolk-msg-overlay";
|
|
664
|
+
overlay.addEventListener("click", (e) => {
|
|
665
|
+
if (e.target === overlay) {
|
|
666
|
+
saveMessageDismissal(message.id);
|
|
667
|
+
options.onDismiss?.(message.id);
|
|
668
|
+
this.dismiss();
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
const card = document.createElement("div");
|
|
672
|
+
card.className = "tolk-msg-card";
|
|
673
|
+
card.style.background = sanitizeCssColor(message.background_color) || "#ffffff";
|
|
674
|
+
const closeBtn = document.createElement("button");
|
|
675
|
+
closeBtn.className = "tolk-msg-close";
|
|
676
|
+
closeBtn.setAttribute("aria-label", "Close message");
|
|
677
|
+
closeBtn.textContent = "\xD7";
|
|
678
|
+
closeBtn.addEventListener("click", () => {
|
|
679
|
+
saveMessageDismissal(message.id);
|
|
680
|
+
options.onDismiss?.(message.id);
|
|
681
|
+
this.dismiss();
|
|
682
|
+
});
|
|
683
|
+
card.appendChild(closeBtn);
|
|
684
|
+
if (message.content && message.content.content) {
|
|
685
|
+
for (const component of message.content.content) {
|
|
686
|
+
const el = this.renderComponent(component, message.id, options);
|
|
687
|
+
if (el) card.appendChild(el);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
overlay.appendChild(card);
|
|
691
|
+
document.head.appendChild(style);
|
|
692
|
+
document.body.appendChild(overlay);
|
|
693
|
+
this.overlay = overlay;
|
|
694
|
+
this.styleEl = style;
|
|
695
|
+
requestAnimationFrame(() => {
|
|
696
|
+
requestAnimationFrame(() => {
|
|
697
|
+
overlay.classList.add("tolk-visible");
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
renderComponent(component, messageId, options) {
|
|
702
|
+
const props = component.props;
|
|
703
|
+
switch (component.type) {
|
|
704
|
+
case "Heading": {
|
|
705
|
+
const el = document.createElement("h2");
|
|
706
|
+
el.textContent = props.text || "";
|
|
707
|
+
el.style.fontSize = (props.fontSize || 28) + "px";
|
|
708
|
+
el.style.fontWeight = "700";
|
|
709
|
+
el.style.color = sanitizeCssColor(props.color) || "#1B1B1B";
|
|
710
|
+
el.style.textAlign = sanitizeTextAlign(props.alignment);
|
|
711
|
+
el.style.lineHeight = "1.2";
|
|
712
|
+
el.style.margin = "0 0 8px 0";
|
|
713
|
+
return el;
|
|
714
|
+
}
|
|
715
|
+
case "TextBlock": {
|
|
716
|
+
const el = document.createElement("p");
|
|
717
|
+
el.textContent = props.content || "";
|
|
718
|
+
el.style.fontSize = (props.fontSize || 15) + "px";
|
|
719
|
+
el.style.color = sanitizeCssColor(props.color) || "#555555";
|
|
720
|
+
el.style.textAlign = sanitizeTextAlign(props.alignment);
|
|
721
|
+
el.style.lineHeight = "1.5";
|
|
722
|
+
el.style.margin = "0 0 8px 0";
|
|
723
|
+
return el;
|
|
724
|
+
}
|
|
725
|
+
case "Image": {
|
|
726
|
+
const url = props.url || "";
|
|
727
|
+
if (!isSafeUrl2(url)) return null;
|
|
728
|
+
const el = document.createElement("img");
|
|
729
|
+
el.src = url;
|
|
730
|
+
el.alt = props.alt || "";
|
|
731
|
+
const width = props.width || "100%";
|
|
732
|
+
el.style.width = width.endsWith("px") || width.endsWith("%") ? width : width + "px";
|
|
733
|
+
el.style.borderRadius = (props.borderRadius || 8) + "px";
|
|
734
|
+
el.style.display = "block";
|
|
735
|
+
el.style.margin = "0 auto 8px auto";
|
|
736
|
+
return el;
|
|
737
|
+
}
|
|
738
|
+
case "Button": {
|
|
739
|
+
const el = document.createElement("button");
|
|
740
|
+
el.textContent = props.label || "Click";
|
|
741
|
+
el.style.backgroundColor = sanitizeCssColor(props.bgColor) || "#1B1B1B";
|
|
742
|
+
el.style.color = sanitizeCssColor(props.textColor) || "#ffffff";
|
|
743
|
+
el.style.fontSize = (props.fontSize || 16) + "px";
|
|
744
|
+
el.style.borderRadius = (props.borderRadius || 8) + "px";
|
|
745
|
+
el.style.border = "none";
|
|
746
|
+
el.style.padding = "10px 20px";
|
|
747
|
+
el.style.cursor = "pointer";
|
|
748
|
+
el.style.fontWeight = "600";
|
|
749
|
+
el.style.margin = "8px 0";
|
|
750
|
+
if (props.fullWidth) {
|
|
751
|
+
el.style.width = "100%";
|
|
752
|
+
}
|
|
753
|
+
el.addEventListener("click", () => {
|
|
754
|
+
const action = props.action || "";
|
|
755
|
+
if (options.onButtonPress) {
|
|
756
|
+
options.onButtonPress(action, messageId);
|
|
757
|
+
} else if (action) {
|
|
758
|
+
if (isSafeUrl2(action)) {
|
|
759
|
+
window.location.href = action;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
return el;
|
|
764
|
+
}
|
|
765
|
+
case "Section": {
|
|
766
|
+
const el = document.createElement("div");
|
|
767
|
+
if (props.bgColor) el.style.backgroundColor = sanitizeCssColor(props.bgColor) || "";
|
|
768
|
+
el.style.padding = (props.padding || 16) + "px";
|
|
769
|
+
el.style.borderRadius = (props.borderRadius || 0) + "px";
|
|
770
|
+
el.style.margin = "8px 0";
|
|
771
|
+
if (props.bgImage) {
|
|
772
|
+
const sanitizedUrl = sanitizeCssUrl(props.bgImage);
|
|
773
|
+
if (sanitizedUrl) {
|
|
774
|
+
el.style.backgroundImage = `url("${sanitizedUrl}")`;
|
|
775
|
+
el.style.backgroundSize = sanitizeBackgroundSize(props.bgSize);
|
|
776
|
+
el.style.backgroundPosition = "center";
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const children = props.children || [];
|
|
780
|
+
for (const child of children) {
|
|
781
|
+
const childEl = this.renderComponent(child, messageId, options);
|
|
782
|
+
if (childEl) el.appendChild(childEl);
|
|
783
|
+
}
|
|
784
|
+
return el;
|
|
785
|
+
}
|
|
786
|
+
case "Spacer": {
|
|
787
|
+
const el = document.createElement("div");
|
|
788
|
+
el.style.height = (props.height || 24) + "px";
|
|
789
|
+
return el;
|
|
790
|
+
}
|
|
791
|
+
case "Divider": {
|
|
792
|
+
const el = document.createElement("hr");
|
|
793
|
+
el.style.border = "none";
|
|
794
|
+
el.style.borderTop = `${props.thickness || 1}px solid ${sanitizeCssColor(props.color) || "#e5e5e5"}`;
|
|
795
|
+
el.style.margin = "8px 0";
|
|
796
|
+
return el;
|
|
797
|
+
}
|
|
798
|
+
default:
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
function isSafeUrl2(url) {
|
|
804
|
+
try {
|
|
805
|
+
const parsed = new URL(url, window.location.href);
|
|
806
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
807
|
+
} catch {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function sanitizeCssUrl(url) {
|
|
812
|
+
if (!isSafeUrl2(url)) return null;
|
|
813
|
+
return url.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "").replace(/\r/g, "");
|
|
814
|
+
}
|
|
815
|
+
function sanitizeTextAlign(value) {
|
|
816
|
+
const allowed = ["left", "center", "right", "justify"];
|
|
817
|
+
if (value && allowed.includes(value)) {
|
|
818
|
+
return value;
|
|
819
|
+
}
|
|
820
|
+
return "left";
|
|
821
|
+
}
|
|
822
|
+
function sanitizeBackgroundSize(value) {
|
|
823
|
+
if (!value) return "cover";
|
|
824
|
+
const allowed = ["cover", "contain", "auto"];
|
|
825
|
+
if (allowed.includes(value)) {
|
|
826
|
+
return value;
|
|
827
|
+
}
|
|
828
|
+
const lengthPattern = /^(\d+(\.\d+)?(px|%|em|rem|vh|vw)(\s+\d+(\.\d+)?(px|%|em|rem|vh|vw))?)$/;
|
|
829
|
+
if (lengthPattern.test(value)) {
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
return "cover";
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/index.ts
|
|
836
|
+
var Tolinku = class {
|
|
837
|
+
constructor(config) {
|
|
838
|
+
/** The current user ID, used for segment targeting and analytics. */
|
|
839
|
+
this._userId = null;
|
|
840
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
841
|
+
throw new Error("Tolinku: apiKey is required and must be a non-empty string");
|
|
842
|
+
}
|
|
843
|
+
const resolvedConfig = {
|
|
844
|
+
...config,
|
|
845
|
+
baseUrl: config.baseUrl || "https://api.tolinku.com"
|
|
846
|
+
};
|
|
847
|
+
try {
|
|
848
|
+
const parsed = new URL(resolvedConfig.baseUrl);
|
|
849
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
850
|
+
throw new Error("Tolinku: baseUrl must use http: or https: protocol");
|
|
851
|
+
}
|
|
852
|
+
} catch (e) {
|
|
853
|
+
if (e instanceof Error && e.message.startsWith("Tolinku:")) throw e;
|
|
854
|
+
throw new Error("Tolinku: baseUrl must be a valid URL (e.g. https://api.tolinku.com)");
|
|
855
|
+
}
|
|
856
|
+
this.client = new HttpClient(resolvedConfig);
|
|
857
|
+
this.analytics = new Analytics(this.client);
|
|
858
|
+
this.referrals = new Referrals(this.client);
|
|
859
|
+
this.deferred = new Deferred(this.client);
|
|
860
|
+
this.banners = new Banners(this.client);
|
|
861
|
+
this.messages = new Messages(this.client);
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Set the user ID for segment targeting and analytics attribution.
|
|
865
|
+
* Pass null to clear the user ID.
|
|
866
|
+
*/
|
|
867
|
+
setUserId(userId) {
|
|
868
|
+
this._userId = userId;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Track a custom event (shorthand for analytics.track).
|
|
872
|
+
* Event type is auto-prefixed with "custom." if not already.
|
|
873
|
+
* If a userId has been set, it is automatically injected into event properties.
|
|
874
|
+
*/
|
|
875
|
+
async track(eventType, properties) {
|
|
876
|
+
const mergedProps = this._userId ? { user_id: this._userId, ...properties } : properties;
|
|
877
|
+
return this.analytics.track(eventType, mergedProps);
|
|
878
|
+
}
|
|
879
|
+
/** Show a smart banner at the top or bottom of the page */
|
|
880
|
+
async showBanner(options) {
|
|
881
|
+
return this.banners.show(options, this._userId);
|
|
882
|
+
}
|
|
883
|
+
/** Dismiss the currently visible smart banner */
|
|
884
|
+
dismissBanner() {
|
|
885
|
+
this.banners.dismiss();
|
|
886
|
+
}
|
|
887
|
+
/** Show an in-app message as a modal overlay */
|
|
888
|
+
async showMessage(options) {
|
|
889
|
+
return this.messages.show(options, this._userId);
|
|
890
|
+
}
|
|
891
|
+
/** Dismiss the currently visible in-app message */
|
|
892
|
+
dismissMessage() {
|
|
893
|
+
this.messages.dismiss();
|
|
894
|
+
}
|
|
895
|
+
/** Flush any queued analytics events immediately */
|
|
896
|
+
async flush() {
|
|
897
|
+
return this.analytics.flush();
|
|
898
|
+
}
|
|
899
|
+
/** Clean up all DOM elements, flush events, and cancel in-flight requests (e.g. before unmounting in SPAs) */
|
|
900
|
+
destroy() {
|
|
901
|
+
this.analytics.destroy();
|
|
902
|
+
this.client.abort();
|
|
903
|
+
this.banners.dismiss();
|
|
904
|
+
this.messages.dismiss();
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
export { Tolinku, TolinkuError };
|
|
909
|
+
//# sourceMappingURL=index.mjs.map
|
|
910
|
+
//# sourceMappingURL=index.mjs.map
|