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