@kitbase/messaging 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/dist/index.cjs ADDED
@@ -0,0 +1,949 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ApiError: () => ApiError,
24
+ AuthenticationError: () => AuthenticationError,
25
+ Messaging: () => Messaging,
26
+ MessagingError: () => MessagingError,
27
+ TimeoutError: () => TimeoutError,
28
+ ValidationError: () => ValidationError,
29
+ getInstance: () => getInstance,
30
+ init: () => init
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/errors.ts
35
+ var MessagingError = class _MessagingError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = "MessagingError";
39
+ Object.setPrototypeOf(this, _MessagingError.prototype);
40
+ }
41
+ };
42
+ var AuthenticationError = class _AuthenticationError extends MessagingError {
43
+ constructor(message = "Invalid API key") {
44
+ super(message);
45
+ this.name = "AuthenticationError";
46
+ Object.setPrototypeOf(this, _AuthenticationError.prototype);
47
+ }
48
+ };
49
+ var ApiError = class _ApiError extends MessagingError {
50
+ statusCode;
51
+ response;
52
+ constructor(message, statusCode, response) {
53
+ super(message);
54
+ this.name = "ApiError";
55
+ this.statusCode = statusCode;
56
+ this.response = response;
57
+ Object.setPrototypeOf(this, _ApiError.prototype);
58
+ }
59
+ };
60
+ var ValidationError = class _ValidationError extends MessagingError {
61
+ field;
62
+ constructor(message, field) {
63
+ super(message);
64
+ this.name = "ValidationError";
65
+ this.field = field;
66
+ Object.setPrototypeOf(this, _ValidationError.prototype);
67
+ }
68
+ };
69
+ var TimeoutError = class _TimeoutError extends MessagingError {
70
+ constructor(message = "Request timed out") {
71
+ super(message);
72
+ this.name = "TimeoutError";
73
+ Object.setPrototypeOf(this, _TimeoutError.prototype);
74
+ }
75
+ };
76
+
77
+ // src/api.ts
78
+ var DEFAULT_BASE_URL = "https://api.kitbase.dev";
79
+ var TIMEOUT = 3e4;
80
+ var MessagingApi = class {
81
+ constructor(sdkKey, baseUrl) {
82
+ this.sdkKey = sdkKey;
83
+ this.baseUrl = (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
84
+ }
85
+ baseUrl;
86
+ async getMessages(options) {
87
+ const params = {};
88
+ if (options?.userId) params.userId = options.userId;
89
+ if (options?.metadata) params.metadata = JSON.stringify(options.metadata);
90
+ const response = await this.get(
91
+ "/sdk/v1/in-app-messages",
92
+ params
93
+ );
94
+ return response.messages.map(toInAppMessage);
95
+ }
96
+ async markViewed(messageId, userId) {
97
+ await this.post("/sdk/v1/in-app-messages/views", { messageId, userId });
98
+ }
99
+ // ── HTTP ──────────────────────────────────────────────────────
100
+ async get(endpoint, params) {
101
+ let url = `${this.baseUrl}${endpoint}`;
102
+ if (params && Object.keys(params).length > 0) {
103
+ const qs = Object.entries(params).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
104
+ url += `?${qs}`;
105
+ }
106
+ const controller = new AbortController();
107
+ const tid = setTimeout(() => controller.abort(), TIMEOUT);
108
+ try {
109
+ const res = await fetch(url, {
110
+ method: "GET",
111
+ headers: { "Content-Type": "application/json", "x-sdk-key": this.sdkKey },
112
+ signal: controller.signal
113
+ });
114
+ clearTimeout(tid);
115
+ if (!res.ok) this.throwError(res, await this.parseBody(res));
116
+ return await res.json();
117
+ } catch (err) {
118
+ clearTimeout(tid);
119
+ if (err instanceof Error && err.name === "AbortError") throw new TimeoutError();
120
+ throw err;
121
+ }
122
+ }
123
+ async post(endpoint, body) {
124
+ const controller = new AbortController();
125
+ const tid = setTimeout(() => controller.abort(), TIMEOUT);
126
+ try {
127
+ const res = await fetch(`${this.baseUrl}${endpoint}`, {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json", "x-sdk-key": this.sdkKey },
130
+ body: JSON.stringify(body),
131
+ signal: controller.signal
132
+ });
133
+ clearTimeout(tid);
134
+ if (!res.ok) this.throwError(res, await this.parseBody(res));
135
+ } catch (err) {
136
+ clearTimeout(tid);
137
+ if (err instanceof Error && err.name === "AbortError") throw new TimeoutError();
138
+ throw err;
139
+ }
140
+ }
141
+ throwError(res, body) {
142
+ if (res.status === 401) throw new AuthenticationError();
143
+ let msg = res.statusText;
144
+ if (body && typeof body === "object") {
145
+ if ("message" in body) msg = String(body.message);
146
+ else if ("error" in body) msg = String(body.error);
147
+ }
148
+ throw new ApiError(msg, res.status, body);
149
+ }
150
+ async parseBody(res) {
151
+ try {
152
+ return await res.json();
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ };
158
+ function toInAppMessage(raw) {
159
+ const msg = {
160
+ id: raw.id,
161
+ title: raw.title,
162
+ body: raw.message,
163
+ showOnce: raw.showOnce,
164
+ type: raw.messageType,
165
+ channel: raw.channelName || null,
166
+ imageUrl: raw.imageUrl,
167
+ backgroundColor: raw.backgroundColor,
168
+ startDate: raw.startDate,
169
+ endDate: raw.endDate
170
+ };
171
+ if (raw.actionButtonText) {
172
+ msg.actionButton = {
173
+ text: raw.actionButtonText,
174
+ url: raw.actionButtonUrl,
175
+ color: raw.actionButtonColor,
176
+ textColor: raw.actionButtonTextColor
177
+ };
178
+ }
179
+ if (raw.secondaryButtonText) {
180
+ msg.secondaryButton = {
181
+ text: raw.secondaryButtonText,
182
+ url: raw.secondaryButtonUrl,
183
+ color: raw.secondaryButtonColor,
184
+ textColor: raw.secondaryButtonTextColor
185
+ };
186
+ }
187
+ return msg;
188
+ }
189
+
190
+ // src/styles.ts
191
+ var STYLES = (
192
+ /* css */
193
+ `
194
+ :host {
195
+ all: initial;
196
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
197
+ font-size: 14px;
198
+ line-height: 1.5;
199
+ color: #1a1a1a;
200
+ -webkit-font-smoothing: antialiased;
201
+ -moz-osx-font-smoothing: grayscale;
202
+ }
203
+
204
+ *, *::before, *::after {
205
+ box-sizing: border-box;
206
+ margin: 0;
207
+ padding: 0;
208
+ }
209
+
210
+ /* ============================== Overlay ============================== */
211
+
212
+ .kb-overlay {
213
+ position: fixed;
214
+ inset: 0;
215
+ z-index: 999999;
216
+ background: rgba(0, 0, 0, 0.5);
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ padding: 16px;
221
+ animation: kb-fade-in 200ms ease-out;
222
+ }
223
+ .kb-overlay.kb-exit {
224
+ animation: kb-fade-out 150ms ease-in forwards;
225
+ }
226
+
227
+ /* ============================== Modal ============================== */
228
+
229
+ .kb-modal {
230
+ position: relative;
231
+ background: #fff;
232
+ border-radius: 16px;
233
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
234
+ max-width: 480px;
235
+ width: 100%;
236
+ overflow: hidden;
237
+ animation: kb-scale-in 200ms ease-out;
238
+ }
239
+ .kb-overlay.kb-exit .kb-modal {
240
+ animation: kb-scale-out 150ms ease-in forwards;
241
+ }
242
+
243
+ /* ============================== Banner ============================== */
244
+
245
+ .kb-banner-container {
246
+ position: fixed;
247
+ top: 0;
248
+ left: 0;
249
+ right: 0;
250
+ z-index: 999998;
251
+ display: flex;
252
+ flex-direction: column;
253
+ pointer-events: none;
254
+ }
255
+
256
+ .kb-banner {
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 16px;
260
+ padding: 12px 20px;
261
+ background: #4F46E5;
262
+ color: #fff;
263
+ pointer-events: auto;
264
+ animation: kb-slide-down 250ms ease-out;
265
+ }
266
+ .kb-banner.kb-exit {
267
+ animation: kb-slide-up-exit 150ms ease-in forwards;
268
+ }
269
+ .kb-banner .kb-content {
270
+ flex: 1;
271
+ min-width: 0;
272
+ }
273
+ .kb-banner .kb-title {
274
+ font-weight: 600;
275
+ font-size: 14px;
276
+ }
277
+ .kb-banner .kb-body {
278
+ font-size: 13px;
279
+ opacity: 0.9;
280
+ color: inherit;
281
+ margin: 0;
282
+ }
283
+ .kb-banner .kb-actions {
284
+ display: flex;
285
+ align-items: center;
286
+ gap: 8px;
287
+ flex-shrink: 0;
288
+ }
289
+ .kb-banner .kb-btn-action {
290
+ background: rgba(255,255,255,0.2);
291
+ color: #fff;
292
+ }
293
+ .kb-banner .kb-btn-secondary {
294
+ background: transparent;
295
+ color: rgba(255,255,255,0.8);
296
+ }
297
+ .kb-banner .kb-close {
298
+ position: static;
299
+ background: rgba(255,255,255,0.15);
300
+ color: #fff;
301
+ }
302
+ .kb-banner .kb-close:hover {
303
+ background: rgba(255,255,255,0.25);
304
+ }
305
+
306
+ /* ============================== Card ============================== */
307
+
308
+ .kb-card-container {
309
+ position: fixed;
310
+ bottom: 24px;
311
+ right: 24px;
312
+ z-index: 999997;
313
+ display: flex;
314
+ flex-direction: column;
315
+ gap: 12px;
316
+ max-width: 360px;
317
+ pointer-events: none;
318
+ }
319
+
320
+ .kb-card {
321
+ position: relative;
322
+ background: #fff;
323
+ border-radius: 16px;
324
+ box-shadow: 0 10px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
325
+ overflow: hidden;
326
+ pointer-events: auto;
327
+ animation: kb-slide-up 250ms ease-out;
328
+ }
329
+ .kb-card.kb-exit {
330
+ animation: kb-slide-down-exit 150ms ease-in forwards;
331
+ }
332
+
333
+ /* ============================== Image (fullscreen) ============================== */
334
+
335
+ .kb-image-msg {
336
+ position: relative;
337
+ max-width: 600px;
338
+ width: 100%;
339
+ animation: kb-scale-in 200ms ease-out;
340
+ }
341
+ .kb-overlay.kb-exit .kb-image-msg {
342
+ animation: kb-scale-out 150ms ease-in forwards;
343
+ }
344
+ .kb-image-msg > img {
345
+ display: block;
346
+ width: 100%;
347
+ border-radius: 16px;
348
+ }
349
+ .kb-image-msg .kb-buttons {
350
+ margin-top: 12px;
351
+ justify-content: center;
352
+ }
353
+
354
+ /* ============================== Shared ============================== */
355
+
356
+ .kb-close {
357
+ position: absolute;
358
+ top: 8px;
359
+ right: 8px;
360
+ width: 28px;
361
+ height: 28px;
362
+ display: flex;
363
+ align-items: center;
364
+ justify-content: center;
365
+ background: rgba(0,0,0,0.06);
366
+ border: none;
367
+ border-radius: 50%;
368
+ cursor: pointer;
369
+ font-size: 18px;
370
+ line-height: 1;
371
+ color: #666;
372
+ transition: background 150ms, color 150ms;
373
+ z-index: 1;
374
+ }
375
+ .kb-close:hover {
376
+ background: rgba(0,0,0,0.12);
377
+ color: #333;
378
+ }
379
+ .kb-overlay .kb-close {
380
+ background: rgba(255,255,255,0.15);
381
+ color: #fff;
382
+ }
383
+ .kb-overlay .kb-close:hover {
384
+ background: rgba(255,255,255,0.25);
385
+ }
386
+
387
+ .kb-msg-image {
388
+ display: block;
389
+ width: 100%;
390
+ }
391
+
392
+ .kb-content {
393
+ padding: 20px;
394
+ }
395
+
396
+ .kb-title {
397
+ font-size: 18px;
398
+ font-weight: 600;
399
+ line-height: 1.3;
400
+ margin-bottom: 6px;
401
+ }
402
+
403
+ .kb-body {
404
+ font-size: 14px;
405
+ color: #555;
406
+ line-height: 1.6;
407
+ margin-bottom: 16px;
408
+ white-space: pre-wrap;
409
+ }
410
+
411
+ .kb-buttons {
412
+ display: flex;
413
+ gap: 8px;
414
+ }
415
+
416
+ .kb-btn {
417
+ display: inline-flex;
418
+ align-items: center;
419
+ justify-content: center;
420
+ padding: 10px 20px;
421
+ border: none;
422
+ border-radius: 10px;
423
+ font-size: 14px;
424
+ font-weight: 500;
425
+ cursor: pointer;
426
+ text-decoration: none;
427
+ transition: opacity 150ms;
428
+ font-family: inherit;
429
+ line-height: 1;
430
+ }
431
+ .kb-btn:hover {
432
+ opacity: 0.88;
433
+ }
434
+
435
+ .kb-btn-action {
436
+ background: #4F46E5;
437
+ color: #fff;
438
+ }
439
+
440
+ .kb-btn-secondary {
441
+ background: #f3f4f6;
442
+ color: #374151;
443
+ }
444
+
445
+ /* ============================== Animations ============================== */
446
+
447
+ @keyframes kb-fade-in {
448
+ from { opacity: 0 } to { opacity: 1 }
449
+ }
450
+ @keyframes kb-fade-out {
451
+ from { opacity: 1 } to { opacity: 0 }
452
+ }
453
+ @keyframes kb-scale-in {
454
+ from { transform: scale(0.95); opacity: 0 }
455
+ to { transform: scale(1); opacity: 1 }
456
+ }
457
+ @keyframes kb-scale-out {
458
+ from { transform: scale(1); opacity: 1 }
459
+ to { transform: scale(0.95); opacity: 0 }
460
+ }
461
+ @keyframes kb-slide-down {
462
+ from { transform: translateY(-100%) }
463
+ to { transform: translateY(0) }
464
+ }
465
+ @keyframes kb-slide-up-exit {
466
+ from { transform: translateY(0) }
467
+ to { transform: translateY(-100%) }
468
+ }
469
+ @keyframes kb-slide-up {
470
+ from { transform: translateY(20px); opacity: 0 }
471
+ to { transform: translateY(0); opacity: 1 }
472
+ }
473
+ @keyframes kb-slide-down-exit {
474
+ from { transform: translateY(0); opacity: 1 }
475
+ to { transform: translateY(20px); opacity: 0 }
476
+ }
477
+
478
+ /* ============================== Responsive ============================== */
479
+
480
+ @media (max-width: 480px) {
481
+ .kb-card-container {
482
+ left: 12px;
483
+ right: 12px;
484
+ bottom: 12px;
485
+ max-width: none;
486
+ }
487
+ .kb-modal {
488
+ max-width: none;
489
+ }
490
+ }
491
+ `
492
+ );
493
+
494
+ // src/renderer.ts
495
+ var MessageRenderer = class {
496
+ constructor(callbacks) {
497
+ this.callbacks = callbacks;
498
+ this.host = document.createElement("div");
499
+ this.host.id = "kitbase-messaging";
500
+ document.body.appendChild(this.host);
501
+ this.shadow = this.host.attachShadow({ mode: "open" });
502
+ const style = document.createElement("style");
503
+ style.textContent = STYLES;
504
+ this.shadow.appendChild(style);
505
+ this.bannerContainer = document.createElement("div");
506
+ this.bannerContainer.className = "kb-banner-container";
507
+ this.shadow.appendChild(this.bannerContainer);
508
+ this.cardContainer = document.createElement("div");
509
+ this.cardContainer.className = "kb-card-container";
510
+ this.shadow.appendChild(this.cardContainer);
511
+ }
512
+ host;
513
+ shadow;
514
+ bannerContainer;
515
+ cardContainer;
516
+ displayed = /* @__PURE__ */ new Map();
517
+ /**
518
+ * Reconcile: add new messages, remove stale ones.
519
+ * Idempotent — calling with the same list is a no-op.
520
+ */
521
+ update(messages) {
522
+ const incoming = new Set(messages.map((m) => m.id));
523
+ for (const [id] of this.displayed) {
524
+ if (!incoming.has(id)) {
525
+ this.removeMessage(id);
526
+ }
527
+ }
528
+ for (const msg of messages) {
529
+ if (!this.displayed.has(msg.id)) {
530
+ this.renderMessage(msg);
531
+ }
532
+ }
533
+ }
534
+ /** Programmatically dismiss a message (with exit animation). */
535
+ dismiss(messageId) {
536
+ this.removeMessage(messageId);
537
+ }
538
+ /** Remove all rendered messages immediately. */
539
+ clear() {
540
+ for (const [id] of this.displayed) {
541
+ const entry = this.displayed.get(id);
542
+ if (entry) entry.element.remove();
543
+ }
544
+ this.displayed.clear();
545
+ }
546
+ /** Remove the shadow host from the DOM entirely. */
547
+ destroy() {
548
+ this.clear();
549
+ this.host.remove();
550
+ }
551
+ // ── Rendering ─────────────────────────────────────────────────
552
+ renderMessage(msg) {
553
+ let element;
554
+ switch (msg.type) {
555
+ case "banner":
556
+ element = this.renderBanner(msg);
557
+ this.bannerContainer.appendChild(element);
558
+ break;
559
+ case "card":
560
+ element = this.renderCard(msg);
561
+ this.cardContainer.appendChild(element);
562
+ break;
563
+ case "image":
564
+ element = this.renderImageOverlay(msg);
565
+ this.shadow.appendChild(element);
566
+ break;
567
+ case "modal":
568
+ default:
569
+ element = this.renderModal(msg);
570
+ this.shadow.appendChild(element);
571
+ break;
572
+ }
573
+ this.displayed.set(msg.id, { element, message: msg });
574
+ this.callbacks.onShow(msg);
575
+ }
576
+ renderModal(msg) {
577
+ const overlay = this.el("div", "kb-overlay");
578
+ const modal = this.el("div", "kb-modal");
579
+ if (msg.backgroundColor) modal.style.background = msg.backgroundColor;
580
+ modal.appendChild(this.closeButton(msg));
581
+ if (msg.imageUrl) {
582
+ const img = document.createElement("img");
583
+ img.className = "kb-msg-image";
584
+ img.src = msg.imageUrl;
585
+ img.alt = "";
586
+ modal.appendChild(img);
587
+ }
588
+ const content = this.el("div", "kb-content");
589
+ content.appendChild(this.titleEl(msg.title));
590
+ if (msg.body) content.appendChild(this.bodyEl(msg.body));
591
+ content.appendChild(this.buttonsEl(msg));
592
+ modal.appendChild(content);
593
+ overlay.addEventListener("click", (e) => {
594
+ if (e.target === overlay) this.handleDismiss(msg);
595
+ });
596
+ overlay.appendChild(modal);
597
+ return overlay;
598
+ }
599
+ renderBanner(msg) {
600
+ const banner = this.el("div", "kb-banner");
601
+ if (msg.backgroundColor) {
602
+ banner.style.background = msg.backgroundColor;
603
+ }
604
+ const content = this.el("div", "kb-content");
605
+ content.appendChild(this.titleEl(msg.title));
606
+ if (msg.body) content.appendChild(this.bodyEl(msg.body));
607
+ banner.appendChild(content);
608
+ const actions = this.el("div", "kb-actions");
609
+ if (msg.actionButton) {
610
+ actions.appendChild(this.btnEl(msg, msg.actionButton, "kb-btn-action"));
611
+ }
612
+ if (msg.secondaryButton) {
613
+ actions.appendChild(this.btnEl(msg, msg.secondaryButton, "kb-btn-secondary"));
614
+ }
615
+ actions.appendChild(this.closeButton(msg));
616
+ banner.appendChild(actions);
617
+ return banner;
618
+ }
619
+ renderCard(msg) {
620
+ const card = this.el("div", "kb-card");
621
+ if (msg.backgroundColor) card.style.background = msg.backgroundColor;
622
+ card.appendChild(this.closeButton(msg));
623
+ if (msg.imageUrl) {
624
+ const img = document.createElement("img");
625
+ img.className = "kb-msg-image";
626
+ img.src = msg.imageUrl;
627
+ img.alt = "";
628
+ card.appendChild(img);
629
+ }
630
+ const content = this.el("div", "kb-content");
631
+ content.appendChild(this.titleEl(msg.title));
632
+ if (msg.body) content.appendChild(this.bodyEl(msg.body));
633
+ content.appendChild(this.buttonsEl(msg));
634
+ card.appendChild(content);
635
+ return card;
636
+ }
637
+ renderImageOverlay(msg) {
638
+ const overlay = this.el("div", "kb-overlay");
639
+ const container = this.el("div", "kb-image-msg");
640
+ container.appendChild(this.closeButton(msg));
641
+ if (msg.imageUrl) {
642
+ const img = document.createElement("img");
643
+ img.src = msg.imageUrl;
644
+ img.alt = msg.title;
645
+ container.appendChild(img);
646
+ }
647
+ const buttons = this.buttonsEl(msg);
648
+ if (buttons.childElementCount > 0) {
649
+ container.appendChild(buttons);
650
+ }
651
+ overlay.addEventListener("click", (e) => {
652
+ if (e.target === overlay) this.handleDismiss(msg);
653
+ });
654
+ overlay.appendChild(container);
655
+ return overlay;
656
+ }
657
+ // ── Shared elements ───────────────────────────────────────────
658
+ titleEl(text) {
659
+ const el = this.el("div", "kb-title");
660
+ el.textContent = text;
661
+ return el;
662
+ }
663
+ bodyEl(text) {
664
+ const el = this.el("div", "kb-body");
665
+ el.textContent = text;
666
+ return el;
667
+ }
668
+ buttonsEl(msg) {
669
+ const wrap = this.el("div", "kb-buttons");
670
+ if (msg.actionButton) {
671
+ wrap.appendChild(this.btnEl(msg, msg.actionButton, "kb-btn-action"));
672
+ }
673
+ if (msg.secondaryButton) {
674
+ wrap.appendChild(this.btnEl(msg, msg.secondaryButton, "kb-btn-secondary"));
675
+ }
676
+ return wrap;
677
+ }
678
+ btnEl(msg, button, className) {
679
+ const btn = document.createElement("button");
680
+ btn.className = `kb-btn ${className}`;
681
+ btn.textContent = button.text;
682
+ if (button.color) btn.style.background = button.color;
683
+ if (button.textColor) btn.style.color = button.textColor;
684
+ btn.addEventListener("click", (e) => {
685
+ e.stopPropagation();
686
+ this.handleAction(msg, button);
687
+ });
688
+ return btn;
689
+ }
690
+ closeButton(msg) {
691
+ const btn = document.createElement("button");
692
+ btn.className = "kb-close";
693
+ btn.innerHTML = "✕";
694
+ btn.setAttribute("aria-label", "Close");
695
+ btn.addEventListener("click", (e) => {
696
+ e.stopPropagation();
697
+ this.handleDismiss(msg);
698
+ });
699
+ return btn;
700
+ }
701
+ // ── Event handling ────────────────────────────────────────────
702
+ handleDismiss(msg) {
703
+ const entry = this.displayed.get(msg.id);
704
+ if (!entry) return;
705
+ this.animateOut(entry.element, msg);
706
+ }
707
+ handleAction(msg, button) {
708
+ const result = this.callbacks.onAction(msg, button);
709
+ if (result !== false && button.url) {
710
+ window.open(button.url, "_blank", "noopener");
711
+ }
712
+ const entry = this.displayed.get(msg.id);
713
+ if (entry) {
714
+ this.animateOut(entry.element, msg);
715
+ }
716
+ }
717
+ // ── Animation + cleanup ───────────────────────────────────────
718
+ removeMessage(id) {
719
+ const entry = this.displayed.get(id);
720
+ if (!entry) return;
721
+ this.animateOut(entry.element, entry.message);
722
+ }
723
+ animateOut(element, msg) {
724
+ if (element.classList.contains("kb-exit")) return;
725
+ element.classList.add("kb-exit");
726
+ const cleanup = () => {
727
+ element.remove();
728
+ this.displayed.delete(msg.id);
729
+ this.callbacks.onDismiss(msg);
730
+ };
731
+ element.addEventListener("animationend", cleanup, { once: true });
732
+ setTimeout(cleanup, 300);
733
+ }
734
+ // ── Helpers ───────────────────────────────────────────────────
735
+ el(tag, className) {
736
+ const el = document.createElement(tag);
737
+ el.className = className;
738
+ return el;
739
+ }
740
+ };
741
+
742
+ // src/client.ts
743
+ var DEFAULT_POLL_INTERVAL = 6e4;
744
+ var _instance = null;
745
+ function init(config) {
746
+ if (_instance) {
747
+ _instance.close();
748
+ }
749
+ _instance = new Messaging(config);
750
+ return _instance;
751
+ }
752
+ function getInstance() {
753
+ return _instance;
754
+ }
755
+ var Messaging = class {
756
+ api;
757
+ renderer = null;
758
+ config;
759
+ pollTimer = null;
760
+ dismissed = /* @__PURE__ */ new Set();
761
+ subscriptionTimers = /* @__PURE__ */ new Set();
762
+ pendingPoll = false;
763
+ visibilityHandler = null;
764
+ userId;
765
+ constructor(config) {
766
+ if (!config.sdkKey) {
767
+ throw new ValidationError("SDK key is required", "sdkKey");
768
+ }
769
+ this.config = config;
770
+ this.userId = config.userId;
771
+ this.api = new MessagingApi(config.sdkKey, config.baseUrl);
772
+ if (config.autoShow !== false && typeof window !== "undefined") {
773
+ this.start();
774
+ }
775
+ }
776
+ // ── Lifecycle ─────────────────────────────────────────────────
777
+ /**
778
+ * Start fetching and rendering messages.
779
+ * Called automatically unless `autoShow: false`.
780
+ */
781
+ start() {
782
+ if (typeof window === "undefined") return;
783
+ if (this.renderer) return;
784
+ this.renderer = new MessageRenderer({
785
+ onShow: (msg) => {
786
+ this.config.onShow?.(msg);
787
+ },
788
+ onDismiss: (msg) => {
789
+ this.dismissed.add(msg.id);
790
+ this.config.onDismiss?.(msg);
791
+ },
792
+ onAction: (msg, btn) => {
793
+ return this.config.onAction?.(msg, btn);
794
+ }
795
+ });
796
+ this.poll();
797
+ const interval = this.config.pollInterval ?? DEFAULT_POLL_INTERVAL;
798
+ if (interval > 0) {
799
+ this.pollTimer = setInterval(() => this.poll(), interval);
800
+ }
801
+ this.visibilityHandler = () => {
802
+ if (document.visibilityState === "visible" && this.pendingPoll) {
803
+ this.pendingPoll = false;
804
+ this.poll();
805
+ }
806
+ };
807
+ document.addEventListener("visibilitychange", this.visibilityHandler);
808
+ }
809
+ /**
810
+ * Stop polling and remove all rendered messages.
811
+ */
812
+ stop() {
813
+ if (this.pollTimer) {
814
+ clearInterval(this.pollTimer);
815
+ this.pollTimer = null;
816
+ }
817
+ if (this.visibilityHandler) {
818
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
819
+ this.visibilityHandler = null;
820
+ }
821
+ this.pendingPoll = false;
822
+ this.renderer?.destroy();
823
+ this.renderer = null;
824
+ }
825
+ /**
826
+ * Stop everything and remove all rendered UI.
827
+ */
828
+ close() {
829
+ this.stop();
830
+ for (const tid of this.subscriptionTimers) {
831
+ clearInterval(tid);
832
+ }
833
+ this.subscriptionTimers.clear();
834
+ this.dismissed.clear();
835
+ }
836
+ // ── User identity ────────────────────────────────────────────
837
+ /**
838
+ * Set the current user ID.
839
+ * Triggers an immediate re-fetch so show-once messages that the user
840
+ * has already viewed are filtered out.
841
+ */
842
+ identify(userId) {
843
+ this.userId = userId;
844
+ this.poll();
845
+ }
846
+ /**
847
+ * Clear the current user ID and reset dismissed messages.
848
+ * Call this on logout.
849
+ */
850
+ reset() {
851
+ this.userId = void 0;
852
+ this.dismissed.clear();
853
+ this.poll();
854
+ }
855
+ /**
856
+ * Record that the current user has viewed a message.
857
+ * The message is optimistically removed from the UI.
858
+ *
859
+ * @throws {ValidationError} When no user ID has been set
860
+ * @throws {ApiError} When the API returns an error
861
+ */
862
+ async markViewed(messageId) {
863
+ if (!this.userId) {
864
+ throw new ValidationError(
865
+ "User ID is required to mark a message as viewed. Call identify() first.",
866
+ "userId"
867
+ );
868
+ }
869
+ this.dismissed.add(messageId);
870
+ this.renderer?.dismiss(messageId);
871
+ await this.api.markViewed(messageId, this.userId);
872
+ }
873
+ // ── Data-only methods ─────────────────────────────────────────
874
+ /**
875
+ * Fetch active messages without rendering.
876
+ * Use this when `autoShow: false` or for custom rendering.
877
+ *
878
+ * @param options - Metadata for targeting evaluation
879
+ * @throws {AuthenticationError} When the API key is invalid
880
+ * @throws {ApiError} When the API returns an error
881
+ * @throws {TimeoutError} When the request times out
882
+ */
883
+ async getMessages(options) {
884
+ return this.api.getMessages(options);
885
+ }
886
+ /**
887
+ * Subscribe to messages with polling (data-only, no rendering).
888
+ * Returns an unsubscribe function.
889
+ *
890
+ * @example
891
+ * ```typescript
892
+ * const unsub = messaging.subscribe(
893
+ * (messages) => renderMyUI(messages),
894
+ * { pollInterval: 30_000 },
895
+ * );
896
+ *
897
+ * // Later
898
+ * unsub();
899
+ * ```
900
+ */
901
+ subscribe(callback, options) {
902
+ const interval = options?.pollInterval ?? DEFAULT_POLL_INTERVAL;
903
+ let active = true;
904
+ const run = async () => {
905
+ if (!active) return;
906
+ try {
907
+ const msgs = await this.api.getMessages(options);
908
+ if (active) callback(msgs);
909
+ } catch {
910
+ }
911
+ };
912
+ run();
913
+ const tid = setInterval(run, interval);
914
+ this.subscriptionTimers.add(tid);
915
+ return () => {
916
+ active = false;
917
+ clearInterval(tid);
918
+ this.subscriptionTimers.delete(tid);
919
+ };
920
+ }
921
+ // ── Internal ──────────────────────────────────────────────────
922
+ async poll() {
923
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
924
+ this.pendingPoll = true;
925
+ return;
926
+ }
927
+ try {
928
+ const messages = await this.api.getMessages({
929
+ userId: this.userId,
930
+ metadata: this.config.metadata
931
+ });
932
+ const visible = messages.filter((m) => !this.dismissed.has(m.id));
933
+ this.renderer?.update(visible);
934
+ } catch {
935
+ }
936
+ }
937
+ };
938
+ // Annotate the CommonJS export names for ESM import in node:
939
+ 0 && (module.exports = {
940
+ ApiError,
941
+ AuthenticationError,
942
+ Messaging,
943
+ MessagingError,
944
+ TimeoutError,
945
+ ValidationError,
946
+ getInstance,
947
+ init
948
+ });
949
+ //# sourceMappingURL=index.cjs.map