@cognior/iap-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.

Potentially problematic release.


This version of @cognior/iap-sdk might be problematic. Click here for more details.

Files changed (60) hide show
  1. package/.github/copilot-instructions.md +95 -0
  2. package/README.md +79 -0
  3. package/TRACKING.md +105 -0
  4. package/USER_CONTEXT_README.md +284 -0
  5. package/package.json +154 -0
  6. package/src/config.ts +25 -0
  7. package/src/core/flowEngine.ts +1833 -0
  8. package/src/core/triggerManager.ts +1011 -0
  9. package/src/experiences/banner.ts +366 -0
  10. package/src/experiences/beacon.ts +668 -0
  11. package/src/experiences/hotspotTour.ts +654 -0
  12. package/src/experiences/hotspots.ts +566 -0
  13. package/src/experiences/modal.ts +1337 -0
  14. package/src/experiences/modalSequence.ts +1247 -0
  15. package/src/experiences/popover.ts +652 -0
  16. package/src/experiences/registry.ts +21 -0
  17. package/src/experiences/survey.ts +1639 -0
  18. package/src/experiences/taskList.ts +625 -0
  19. package/src/experiences/tooltip.ts +740 -0
  20. package/src/experiences/types.ts +395 -0
  21. package/src/experiences/walkthrough.ts +670 -0
  22. package/src/flow-sequence.ts +177 -0
  23. package/src/flows.ts +512 -0
  24. package/src/http.ts +61 -0
  25. package/src/index.ts +355 -0
  26. package/src/services/flowManager.ts +905 -0
  27. package/src/services/flowNormalizer.ts +74 -0
  28. package/src/services/locationContextService.ts +189 -0
  29. package/src/services/pageContextService.ts +221 -0
  30. package/src/services/userContextService.ts +286 -0
  31. package/src/state/appState.ts +0 -0
  32. package/src/state/hooks.ts +0 -0
  33. package/src/state/index.ts +0 -0
  34. package/src/state/migration.ts +0 -0
  35. package/src/state/store.ts +0 -0
  36. package/src/styles/banner.css.ts +0 -0
  37. package/src/styles/hotspot.css.ts +0 -0
  38. package/src/styles/hotspotTour.css.ts +0 -0
  39. package/src/styles/modal.css.ts +564 -0
  40. package/src/styles/survey.css.ts +1013 -0
  41. package/src/styles/taskList.css.ts +0 -0
  42. package/src/styles/tooltip.css.ts +149 -0
  43. package/src/styles/walkthrough.css.ts +0 -0
  44. package/src/tourUtils.ts +0 -0
  45. package/src/tracking.ts +223 -0
  46. package/src/utils/debounce.ts +66 -0
  47. package/src/utils/eventSequenceValidator.ts +124 -0
  48. package/src/utils/flowTrackingSystem.ts +524 -0
  49. package/src/utils/idGenerator.ts +155 -0
  50. package/src/utils/immediateValidationPrevention.ts +184 -0
  51. package/src/utils/normalize.ts +50 -0
  52. package/src/utils/privacyManager.ts +166 -0
  53. package/src/utils/ruleEvaluator.ts +199 -0
  54. package/src/utils/sanitize.ts +79 -0
  55. package/src/utils/selectors.ts +107 -0
  56. package/src/utils/stepExecutor.ts +345 -0
  57. package/src/utils/triggerNormalizer.ts +149 -0
  58. package/src/utils/validationInterceptor.ts +650 -0
  59. package/tsconfig.json +13 -0
  60. package/tsup.config.ts +13 -0
@@ -0,0 +1,566 @@
1
+ // src/experiences/hotspots.ts
2
+ // Interactive hotspots experience renderer
3
+
4
+ import { sanitizeHtml } from "../utils/sanitize";
5
+ import { register } from "./registry";
6
+ import { resolveSelector } from "../utils/selectors";
7
+ import { waitForElement } from "../utils/triggerNormalizer";
8
+ import type { HotspotsPayload, HotspotItem, CornerPlacement } from "./types";
9
+
10
+ type HotspotsFlow = { id: string; type: "hotspots"; payload: HotspotsPayload };
11
+
12
+ const hotspotsCssText = `
13
+ :root {
14
+ --dap-z-hotspots: 2147483630;
15
+ --dap-hotspot-primary: #3b82f6;
16
+ --dap-hotspot-bg: #ffffff;
17
+ --dap-hotspot-border: #e2e8f0;
18
+ --dap-hotspot-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
19
+ --dap-hotspot-text: #1e293b;
20
+ --dap-hotspot-text-muted: #64748b;
21
+ }
22
+
23
+ .dap-hotspots-overlay {
24
+ position: fixed;
25
+ inset: 0;
26
+ background: rgba(15, 23, 42, 0.4);
27
+ z-index: var(--dap-z-hotspots);
28
+ pointer-events: none;
29
+ opacity: 0;
30
+ animation: hotspotsOverlayFadeIn 0.3s ease-out forwards;
31
+ }
32
+
33
+ @keyframes hotspotsOverlayFadeIn {
34
+ to { opacity: 1; }
35
+ }
36
+
37
+ .dap-hotspot-marker {
38
+ position: absolute;
39
+ width: 24px;
40
+ height: 24px;
41
+ background: var(--dap-hotspot-primary);
42
+ border: 3px solid white;
43
+ border-radius: 50%;
44
+ cursor: pointer;
45
+ z-index: calc(var(--dap-z-hotspots) + 1);
46
+ animation: hotspotPulse 2s infinite;
47
+ transition: all 0.2s ease;
48
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
49
+ }
50
+
51
+ .dap-hotspot-marker:hover {
52
+ transform: scale(1.1);
53
+ animation-play-state: paused;
54
+ }
55
+
56
+ .dap-hotspot-marker.completed {
57
+ background: #10b981;
58
+ animation: none;
59
+ }
60
+
61
+ .dap-hotspot-marker.required {
62
+ background: #f59e0b;
63
+ animation-duration: 1.5s;
64
+ }
65
+
66
+ @keyframes hotspotPulse {
67
+ 0% {
68
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 0 0 0 rgba(59, 130, 246, 0.7);
69
+ }
70
+ 70% {
71
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 0 0 10px rgba(59, 130, 246, 0);
72
+ }
73
+ 100% {
74
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), 0 0 0 0 rgba(59, 130, 246, 0);
75
+ }
76
+ }
77
+
78
+ .dap-hotspot-tooltip {
79
+ position: absolute;
80
+ background: var(--dap-hotspot-bg);
81
+ border: 1px solid var(--dap-hotspot-border);
82
+ border-radius: 8px;
83
+ box-shadow: var(--dap-hotspot-shadow);
84
+ padding: 16px;
85
+ max-width: 320px;
86
+ z-index: calc(var(--dap-z-hotspots) + 2);
87
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
88
+ opacity: 0;
89
+ transform: scale(0.9);
90
+ animation: hotspotTooltipIn 0.2s ease-out forwards;
91
+ pointer-events: auto;
92
+ }
93
+
94
+ @keyframes hotspotTooltipIn {
95
+ to {
96
+ opacity: 1;
97
+ transform: scale(1);
98
+ }
99
+ }
100
+
101
+ .dap-hotspot-tooltip::before {
102
+ content: '';
103
+ position: absolute;
104
+ width: 0;
105
+ height: 0;
106
+ border: 8px solid transparent;
107
+ }
108
+
109
+ .dap-hotspot-tooltip.top::before {
110
+ bottom: -16px;
111
+ left: 50%;
112
+ transform: translateX(-50%);
113
+ border-top-color: var(--dap-hotspot-bg);
114
+ }
115
+
116
+ .dap-hotspot-tooltip.bottom::before {
117
+ top: -16px;
118
+ left: 50%;
119
+ transform: translateX(-50%);
120
+ border-bottom-color: var(--dap-hotspot-bg);
121
+ }
122
+
123
+ .dap-hotspot-tooltip.left::before {
124
+ right: -16px;
125
+ top: 50%;
126
+ transform: translateY(-50%);
127
+ border-left-color: var(--dap-hotspot-bg);
128
+ }
129
+
130
+ .dap-hotspot-tooltip.right::before {
131
+ left: -16px;
132
+ top: 50%;
133
+ transform: translateY(-50%);
134
+ border-right-color: var(--dap-hotspot-bg);
135
+ }
136
+
137
+ .dap-hotspot-title {
138
+ font-size: 16px;
139
+ font-weight: 600;
140
+ color: var(--dap-hotspot-text);
141
+ margin: 0 0 8px 0;
142
+ }
143
+
144
+ .dap-hotspot-description {
145
+ font-size: 14px;
146
+ color: var(--dap-hotspot-text-muted);
147
+ line-height: 1.4;
148
+ margin: 0 0 12px 0;
149
+ }
150
+
151
+ .dap-hotspot-actions {
152
+ display: flex;
153
+ gap: 8px;
154
+ justify-content: flex-end;
155
+ }
156
+
157
+ .dap-hotspot-btn {
158
+ padding: 6px 12px;
159
+ border: 1px solid var(--dap-hotspot-primary);
160
+ border-radius: 4px;
161
+ background: transparent;
162
+ color: var(--dap-hotspot-primary);
163
+ font-size: 12px;
164
+ font-weight: 500;
165
+ cursor: pointer;
166
+ transition: all 0.15s ease;
167
+ }
168
+
169
+ .dap-hotspot-btn:hover {
170
+ background: var(--dap-hotspot-primary);
171
+ color: white;
172
+ }
173
+
174
+ .dap-hotspot-btn.primary {
175
+ background: var(--dap-hotspot-primary);
176
+ color: white;
177
+ }
178
+
179
+ .dap-hotspot-btn.primary:hover {
180
+ background: #2563eb;
181
+ border-color: #2563eb;
182
+ }
183
+
184
+ .dap-hotspots-progress {
185
+ position: fixed;
186
+ top: 20px;
187
+ right: 20px;
188
+ background: var(--dap-hotspot-bg);
189
+ border: 1px solid var(--dap-hotspot-border);
190
+ border-radius: 8px;
191
+ padding: 12px 16px;
192
+ z-index: calc(var(--dap-z-hotspots) + 1);
193
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
194
+ box-shadow: var(--dap-hotspot-shadow);
195
+ animation: hotspotProgressIn 0.3s ease-out forwards;
196
+ opacity: 0;
197
+ }
198
+
199
+ @keyframes hotspotProgressIn {
200
+ to { opacity: 1; }
201
+ }
202
+
203
+ .dap-hotspots-progress-text {
204
+ font-size: 14px;
205
+ color: var(--dap-hotspot-text);
206
+ margin-bottom: 4px;
207
+ }
208
+
209
+ .dap-hotspots-progress-bar {
210
+ width: 120px;
211
+ height: 4px;
212
+ background: var(--dap-hotspot-border);
213
+ border-radius: 2px;
214
+ overflow: hidden;
215
+ }
216
+
217
+ .dap-hotspots-progress-fill {
218
+ height: 100%;
219
+ background: var(--dap-hotspot-primary);
220
+ border-radius: 2px;
221
+ transition: width 0.3s ease;
222
+ width: 0%;
223
+ }
224
+
225
+ .dap-hotspots-controls {
226
+ position: fixed;
227
+ bottom: 20px;
228
+ right: 20px;
229
+ display: flex;
230
+ gap: 8px;
231
+ z-index: calc(var(--dap-z-hotspots) + 1);
232
+ }
233
+
234
+ .dap-hotspots-skip {
235
+ padding: 8px 16px;
236
+ background: var(--dap-hotspot-bg);
237
+ border: 1px solid var(--dap-hotspot-border);
238
+ border-radius: 6px;
239
+ color: var(--dap-hotspot-text);
240
+ font-size: 14px;
241
+ cursor: pointer;
242
+ transition: all 0.15s ease;
243
+ box-shadow: var(--dap-hotspot-shadow);
244
+ }
245
+
246
+ .dap-hotspots-skip:hover {
247
+ background: var(--dap-hotspot-border);
248
+ }
249
+
250
+ @media (max-width: 640px) {
251
+ .dap-hotspots-progress {
252
+ top: 10px;
253
+ right: 10px;
254
+ padding: 8px 12px;
255
+ }
256
+
257
+ .dap-hotspots-controls {
258
+ bottom: 10px;
259
+ right: 10px;
260
+ }
261
+
262
+ .dap-hotspot-tooltip {
263
+ max-width: 280px;
264
+ padding: 12px;
265
+ }
266
+ }
267
+ `;
268
+
269
+ export function registerHotspots() {
270
+ register("hotspots", renderHotspots);
271
+ }
272
+
273
+ export async function renderHotspots(flow: HotspotsFlow): Promise<void> {
274
+ const { payload, id } = flow;
275
+
276
+ // Extract completion tracker
277
+ const completionTracker = payload._completionTracker;
278
+
279
+ // Ensure CSS is injected
280
+ ensureStyles();
281
+
282
+ // Track completed hotspots
283
+ const completedHotspots = new Set<string>();
284
+ let currentTooltip: HTMLElement | null = null;
285
+
286
+ // Create overlay
287
+ const overlay = document.createElement("div");
288
+ overlay.className = "dap-hotspots-overlay";
289
+ document.documentElement.appendChild(overlay);
290
+
291
+ // Create progress indicator if enabled
292
+ let progressEl: HTMLElement | null = null;
293
+ if (payload.showProgress) {
294
+ progressEl = createProgressIndicator(payload);
295
+ document.documentElement.appendChild(progressEl);
296
+ }
297
+
298
+ // Create skip controls if allowed
299
+ let skipEl: HTMLElement | null = null;
300
+ if (payload.allowSkip) {
301
+ skipEl = createSkipControls();
302
+ document.documentElement.appendChild(skipEl);
303
+ skipEl.addEventListener("click", completeHotspots);
304
+ }
305
+
306
+ // Create hotspot markers
307
+ const markers: HTMLElement[] = [];
308
+ const waitForElements = payload.hotspots.map(async (hotspot) => {
309
+ try {
310
+ const element = await waitForElement(hotspot.selector, { timeout: 2000 });
311
+ if (element && element instanceof HTMLElement) {
312
+ createHotspotMarker(hotspot, element);
313
+ }
314
+ } catch (error) {
315
+ console.warn(`[DAP] Failed to find element for hotspot: ${hotspot.selector}`);
316
+ }
317
+ });
318
+
319
+ await Promise.allSettled(waitForElements);
320
+
321
+ function createHotspotMarker(hotspot: HotspotItem, element: HTMLElement) {
322
+ const rect = element.getBoundingClientRect();
323
+ const marker = document.createElement("div");
324
+ marker.className = "dap-hotspot-marker";
325
+ marker.dataset.hotspotId = hotspot.id;
326
+
327
+ if (hotspot.required) {
328
+ marker.classList.add("required");
329
+ }
330
+
331
+ if (hotspot.pulseColor) {
332
+ marker.style.background = hotspot.pulseColor;
333
+ }
334
+
335
+ // Position marker at element center
336
+ marker.style.left = `${rect.left + window.scrollX + rect.width / 2 - 12}px`;
337
+ marker.style.top = `${rect.top + window.scrollY + rect.height / 2 - 12}px`;
338
+
339
+ marker.addEventListener("click", () => showTooltip(hotspot, marker, element));
340
+
341
+ document.documentElement.appendChild(marker);
342
+ markers.push(marker);
343
+ }
344
+
345
+ function showTooltip(hotspot: HotspotItem, marker: HTMLElement, element: HTMLElement) {
346
+ // Hide current tooltip if any
347
+ if (currentTooltip) {
348
+ currentTooltip.remove();
349
+ }
350
+
351
+ const tooltip = document.createElement("div");
352
+ tooltip.className = "dap-hotspot-tooltip";
353
+
354
+ const title = document.createElement("h3");
355
+ title.className = "dap-hotspot-title";
356
+ title.textContent = hotspot.title;
357
+
358
+ const description = document.createElement("div");
359
+ description.className = "dap-hotspot-description";
360
+ description.innerHTML = sanitizeHtml(hotspot.description);
361
+
362
+ const actions = document.createElement("div");
363
+ actions.className = "dap-hotspot-actions";
364
+
365
+ const gotItBtn = document.createElement("button");
366
+ gotItBtn.className = "dap-hotspot-btn primary";
367
+ gotItBtn.textContent = "Got it!";
368
+ gotItBtn.addEventListener("click", () => {
369
+ markHotspotCompleted(hotspot, marker);
370
+ tooltip.remove();
371
+ currentTooltip = null;
372
+ });
373
+
374
+ actions.appendChild(gotItBtn);
375
+ tooltip.appendChild(title);
376
+ tooltip.appendChild(description);
377
+ tooltip.appendChild(actions);
378
+
379
+ // Position tooltip
380
+ positionTooltip(tooltip, marker, element, hotspot.placement || "top");
381
+
382
+ document.documentElement.appendChild(tooltip);
383
+ currentTooltip = tooltip;
384
+
385
+ // Click outside to close
386
+ setTimeout(() => {
387
+ const closeOnOutside = (e: MouseEvent) => {
388
+ if (!tooltip.contains(e.target as Node) && !marker.contains(e.target as Node)) {
389
+ tooltip.remove();
390
+ currentTooltip = null;
391
+ document.removeEventListener("click", closeOnOutside);
392
+ }
393
+ };
394
+ document.addEventListener("click", closeOnOutside);
395
+ }, 100);
396
+ }
397
+
398
+ function positionTooltip(tooltip: HTMLElement, marker: HTMLElement, element: HTMLElement, placement: CornerPlacement) {
399
+ const rect = element.getBoundingClientRect();
400
+ const markerRect = marker.getBoundingClientRect();
401
+
402
+ // Apply placement class for arrow positioning
403
+ tooltip.classList.add(placement);
404
+
405
+ let left = 0;
406
+ let top = 0;
407
+
408
+ switch (placement) {
409
+ case "top":
410
+ left = markerRect.left - 160 + markerRect.width / 2;
411
+ top = markerRect.top - 16 - 120; // tooltip height estimate
412
+ break;
413
+ case "bottom":
414
+ left = markerRect.left - 160 + markerRect.width / 2;
415
+ top = markerRect.bottom + 16;
416
+ break;
417
+ case "left":
418
+ left = markerRect.left - 320 - 16;
419
+ top = markerRect.top - 60 + markerRect.height / 2;
420
+ break;
421
+ case "right":
422
+ left = markerRect.right + 16;
423
+ top = markerRect.top - 60 + markerRect.height / 2;
424
+ break;
425
+ default:
426
+ left = markerRect.left - 160 + markerRect.width / 2;
427
+ top = markerRect.top - 16 - 120;
428
+ }
429
+
430
+ // Keep within viewport bounds
431
+ const viewportWidth = window.innerWidth;
432
+ const viewportHeight = window.innerHeight;
433
+
434
+ left = Math.max(10, Math.min(left, viewportWidth - 330));
435
+ top = Math.max(10, Math.min(top, viewportHeight - 150));
436
+
437
+ tooltip.style.left = `${left + window.scrollX}px`;
438
+ tooltip.style.top = `${top + window.scrollY}px`;
439
+ }
440
+
441
+ function markHotspotCompleted(hotspot: HotspotItem, marker: HTMLElement) {
442
+ completedHotspots.add(hotspot.id);
443
+ marker.classList.add("completed");
444
+
445
+ // Update progress
446
+ if (progressEl) {
447
+ updateProgress();
448
+ }
449
+
450
+ // Check if all required hotspots are completed
451
+ const requiredHotspots = payload.hotspots.filter(h => h.required);
452
+ const completedRequired = requiredHotspots.filter(h => completedHotspots.has(h.id));
453
+
454
+ if (completedRequired.length === requiredHotspots.length) {
455
+ // Allow completion after a short delay
456
+ setTimeout(() => {
457
+ if (canComplete()) {
458
+ completeHotspots();
459
+ }
460
+ }, 1000);
461
+ }
462
+ }
463
+
464
+ function updateProgress() {
465
+ if (!progressEl) return;
466
+
467
+ const total = payload.hotspots.length;
468
+ const completed = completedHotspots.size;
469
+ const percentage = (completed / total) * 100;
470
+
471
+ const progressText = progressEl.querySelector(".dap-hotspots-progress-text") as HTMLElement;
472
+ const progressFill = progressEl.querySelector(".dap-hotspots-progress-fill") as HTMLElement;
473
+
474
+ if (progressText) {
475
+ progressText.textContent = `${completed} / ${total} explored`;
476
+ }
477
+ if (progressFill) {
478
+ progressFill.style.width = `${percentage}%`;
479
+ }
480
+ }
481
+
482
+ function canComplete(): boolean {
483
+ const requiredHotspots = payload.hotspots.filter(h => h.required);
484
+ return requiredHotspots.every(h => completedHotspots.has(h.id));
485
+ }
486
+
487
+ function completeHotspots() {
488
+ // Clean up
489
+ overlay.remove();
490
+ markers.forEach(marker => marker.remove());
491
+ if (currentTooltip) {
492
+ currentTooltip.remove();
493
+ }
494
+ if (progressEl) {
495
+ progressEl.remove();
496
+ }
497
+ if (skipEl) {
498
+ skipEl.remove();
499
+ }
500
+
501
+ // Signal completion
502
+ if (completionTracker?.onComplete) {
503
+ console.debug(`[DAP] Completing hotspots flow: ${id}`);
504
+ completionTracker.onComplete();
505
+ }
506
+ }
507
+
508
+ // Keyboard accessibility
509
+ document.addEventListener("keydown", (e) => {
510
+ if (e.key === "Escape") {
511
+ if (currentTooltip) {
512
+ currentTooltip.remove();
513
+ currentTooltip = null;
514
+ } else if (payload.allowSkip) {
515
+ completeHotspots();
516
+ }
517
+ }
518
+ });
519
+
520
+ // Update progress initially
521
+ if (progressEl) {
522
+ updateProgress();
523
+ }
524
+ }
525
+
526
+ function createProgressIndicator(payload: HotspotsPayload): HTMLElement {
527
+ const progress = document.createElement("div");
528
+ progress.className = "dap-hotspots-progress";
529
+
530
+ const text = document.createElement("div");
531
+ text.className = "dap-hotspots-progress-text";
532
+ text.textContent = "0 / " + payload.hotspots.length + " explored";
533
+
534
+ const bar = document.createElement("div");
535
+ bar.className = "dap-hotspots-progress-bar";
536
+
537
+ const fill = document.createElement("div");
538
+ fill.className = "dap-hotspots-progress-fill";
539
+
540
+ bar.appendChild(fill);
541
+ progress.appendChild(text);
542
+ progress.appendChild(bar);
543
+
544
+ return progress;
545
+ }
546
+
547
+ function createSkipControls(): HTMLElement {
548
+ const controls = document.createElement("div");
549
+ controls.className = "dap-hotspots-controls";
550
+
551
+ const skipBtn = document.createElement("button");
552
+ skipBtn.className = "dap-hotspots-skip";
553
+ skipBtn.textContent = "Skip tour";
554
+
555
+ controls.appendChild(skipBtn);
556
+ return controls;
557
+ }
558
+
559
+ function ensureStyles() {
560
+ if (!document.getElementById("dap-hotspots-style")) {
561
+ const style = document.createElement("style");
562
+ style.id = "dap-hotspots-style";
563
+ style.textContent = hotspotsCssText;
564
+ document.head.appendChild(style);
565
+ }
566
+ }