@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
package/src/http.ts ADDED
@@ -0,0 +1,61 @@
1
+ // src/http.ts
2
+ // Tiny fetch wrapper with headers, JSON body, timeout and better error shape.
3
+
4
+ import type { DapConfig } from "./config";
5
+
6
+ export type HttpOptions = {
7
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
8
+ headers?: Record<string, string>;
9
+ body?: any; // will be JSON-stringified if provided and method != GET
10
+ hostBase?: string; // used for X-Host-Url
11
+ includeHostHeader?: boolean;
12
+ timeoutMs?: number;
13
+ };
14
+
15
+ export async function http(cfg: DapConfig, path: string, opts: HttpOptions = {}): Promise<any> {
16
+ const method = (opts.method || "GET").toUpperCase() as HttpOptions["method"];
17
+ const headers: Record<string, string> = {
18
+ "X-Api-Key": cfg.apikey,
19
+ ...(opts.includeHostHeader && opts.hostBase ? { "X-Host-Url": opts.hostBase } : {}),
20
+ ...(opts.headers || {}),
21
+ };
22
+
23
+ // Only attach JSON body for non-GET
24
+ let bodyInit: BodyInit | undefined;
25
+ if (method !== "GET" && opts.body !== undefined) {
26
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
27
+ bodyInit = JSON.stringify(opts.body);
28
+ }
29
+
30
+ // IMPORTANT: do NOT prefix '/api' here; use the path as given.
31
+ const url = isAbsoluteUrl(path) ? path : new URL(path, location.origin).toString();
32
+
33
+ // Timeout support
34
+ const c = new AbortController();
35
+ const t = setTimeout(() => c.abort(), opts.timeoutMs ?? 15000);
36
+
37
+ let res: Response;
38
+ try {
39
+ res = await fetch(url, { method, headers, body: bodyInit, signal: c.signal, credentials: "omit", cache: "no-cache" });
40
+ } catch (err) {
41
+ clearTimeout(t);
42
+ throw err;
43
+ }
44
+ clearTimeout(t);
45
+
46
+ if (!res.ok) {
47
+ const e = new Error(`HTTP ${res.status}`);
48
+ (e as any).status = res.status;
49
+ try { (e as any).body = await res.text(); } catch {}
50
+ throw e;
51
+ }
52
+
53
+ const ct = res.headers.get("content-type") || "";
54
+ if (ct.includes("application/json")) return res.json();
55
+ if (ct.startsWith("text/")) return res.text();
56
+ return res;
57
+ }
58
+
59
+ function isAbsoluteUrl(u: string): boolean {
60
+ return /^https?:\/\//i.test(u) || u.startsWith("blob:") || u.startsWith("data:");
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,355 @@
1
+ // src/index.ts - Completely refactored DAP SDK
2
+
3
+ // CRITICAL: Import immediate validation prevention FIRST
4
+ import "./utils/immediateValidationPrevention";
5
+
6
+ import type { DapConfig } from "./config";
7
+ import {
8
+ fetchVisibleFlowIds,
9
+ fetchFlowById,
10
+ } from "./flows";
11
+ import { registerModalSequence } from "./experiences/modalSequence";
12
+ import { registerModal, renderModal } from "./experiences/modal";
13
+ import { registerTooltip } from "./experiences/tooltip";
14
+ import { registerSurvey } from "./experiences/survey";
15
+ import { registerPopover } from "./experiences/popover";
16
+ import { registerBeacon } from "./experiences/beacon";
17
+ import { registerBanner } from "./experiences/banner";
18
+ import { registerHotspots } from "./experiences/hotspots";
19
+ import { registerHotspotTour } from "./experiences/hotspotTour";
20
+ import { registerTaskList } from "./experiences/taskList";
21
+ import { registerWalkthrough } from "./experiences/walkthrough";
22
+ import { LocationContextService } from "./services/locationContextService";
23
+ import { userContextService, type DapUser } from "./services/userContextService";
24
+ import { flowEngine } from "./core/flowEngine";
25
+ import type { FlowData } from "./core/flowEngine";
26
+ import { ValidationInterceptor } from "./utils/validationInterceptor";
27
+
28
+ // Initialize validation interceptor for DAP tooltip replacement
29
+ const validationInterceptor = ValidationInterceptor.getInstance();
30
+
31
+ // Register all experience renderers
32
+ registerModalSequence();
33
+ registerModal();
34
+ registerTooltip();
35
+ registerSurvey(); // Now handles both modal surveys and micro surveys
36
+ registerPopover();
37
+ registerBeacon();
38
+ registerBanner();
39
+ registerHotspots();
40
+ registerHotspotTour();
41
+ registerTaskList();
42
+ registerWalkthrough();
43
+
44
+ const log = (...args: any[]) =>
45
+ (window as any).__DAP_DEBUG__ ? console.log("[DAP]", ...args) : void 0;
46
+
47
+ // Global storage for SDK state
48
+ let _dapConfig: DapConfig | null = null;
49
+ let _flowInitializationPending = false;
50
+ let _pendingFlowIds: string[] = [];
51
+ let _registeredFlows: Map<string, FlowData> = new Map(); // Store registered flows in memory
52
+
53
+ /**
54
+ * Initialize DAP SDK with optional user context
55
+ */
56
+ export async function init(opts: {
57
+ configUrl: string;
58
+ debug?: boolean;
59
+ screenId?: string;
60
+ user?: DapUser;
61
+ }) {
62
+ const { configUrl, debug, screenId, user } = opts || ({} as any);
63
+ (window as any).__DAP_DEBUG__ = !!debug;
64
+
65
+ if (!configUrl) throw new Error("DAP.init: configUrl is required");
66
+
67
+ // Extract the pathname for location context
68
+ const pathname = location.pathname.replace(/^\/+/, '');
69
+ const cfg = await loadConfig(configUrl);
70
+ const hostBase = location.origin;
71
+
72
+ // Store config globally
73
+ (window as any).__DAP_CONFIG__ = cfg;
74
+ _dapConfig = cfg;
75
+
76
+ // Set user context if provided
77
+ if (user) {
78
+ userContextService.setUser(user);
79
+ log("User context set during init:", user.id);
80
+ }
81
+
82
+ log("Loaded config", { cfg, hostBase });
83
+
84
+ // Initialize validation interceptor for DAP tooltip replacement (prevention handled elsewhere)
85
+ validationInterceptor.initialize();
86
+
87
+ // Set up location context with current pathname and screenId if provided
88
+ const locationService = LocationContextService.getInstance();
89
+ locationService.setContext({
90
+ currentPath: pathname,
91
+ screenId: screenId || pathname
92
+ });
93
+
94
+ log("Location context set", locationService.getContext());
95
+
96
+ // 1) Get visible flow IDs
97
+ const ids = await fetchVisibleFlowIds(cfg, hostBase, pathname);
98
+ log("Visible flow IDs", ids);
99
+
100
+ if (ids.length === 0) {
101
+ log("No flows available");
102
+ return;
103
+ }
104
+
105
+ // Store pending flow IDs for later initialization
106
+ _pendingFlowIds = ids;
107
+
108
+ // Initialize flows based on readiness state
109
+ await initializeFlowsWhenReady();
110
+ }
111
+
112
+ async function loadConfig(configUrl: string): Promise<DapConfig> {
113
+ const res = await fetch(configUrl);
114
+ if (!res.ok) throw new Error(`Failed to load config: ${res.status}`);
115
+ return res.json();
116
+ }
117
+
118
+ /**
119
+ * Initialize flows when system is ready
120
+ * Waits for DOM readiness and handles user context requirements
121
+ */
122
+ async function initializeFlowsWhenReady(): Promise<void> {
123
+ if (_flowInitializationPending) {
124
+ log("Flow initialization already pending");
125
+ return;
126
+ }
127
+
128
+ _flowInitializationPending = true;
129
+
130
+ // Wait for DOM to be ready
131
+ await waitForDOMReady();
132
+
133
+ // Check if we should proceed with flow initialization
134
+ if (!shouldInitializeFlows()) {
135
+ log("Flow initialization deferred - waiting for user context");
136
+ return;
137
+ }
138
+
139
+ await startPendingFlows();
140
+ }
141
+
142
+ /**
143
+ * Wait for DOM to be ready
144
+ */
145
+ async function waitForDOMReady(): Promise<void> {
146
+ return new Promise((resolve) => {
147
+ if (document.readyState === 'loading') {
148
+ document.addEventListener('DOMContentLoaded', () => resolve());
149
+ } else {
150
+ resolve();
151
+ }
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Determine if flows should initialize now
157
+ */
158
+ function shouldInitializeFlows(): boolean {
159
+ // Always allow if user context is available
160
+ if (userContextService.hasRealUser()) {
161
+ log("Flow initialization allowed - real user context available");
162
+ return true;
163
+ }
164
+
165
+ // For now, allow anonymous flows (can be made more restrictive)
166
+ log("Flow initialization allowed - proceeding with anonymous context");
167
+ return true;
168
+ }
169
+
170
+ /**
171
+ * Start pending flows
172
+ */
173
+ async function startPendingFlows(): Promise<void> {
174
+ if (!_dapConfig || _pendingFlowIds.length === 0) {
175
+ log("No pending flows to start");
176
+ _flowInitializationPending = false;
177
+ return;
178
+ }
179
+
180
+ try {
181
+ const firstFlowId = _pendingFlowIds[0];
182
+ log(`Starting first flow: ${firstFlowId}`);
183
+
184
+ const rawFlowData = await fetchFlowById(_dapConfig, location.origin, firstFlowId);
185
+ console.debug(`[DAP] Raw flow data received:`, rawFlowData);
186
+
187
+ const flowData: FlowData = {
188
+ flowId: rawFlowData.flowId,
189
+ flowName: rawFlowData.flowName || rawFlowData.name || firstFlowId,
190
+ steps: rawFlowData.steps || []
191
+ };
192
+
193
+ console.debug(`[DAP] Converted flow data:`, flowData);
194
+ log("Starting flow with engine:", flowData);
195
+
196
+ await flowEngine.startFlow(flowData);
197
+
198
+ } catch (err) {
199
+ console.error("[DAP] Failed to start initial flow", err);
200
+ } finally {
201
+ _flowInitializationPending = false;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Set user context and trigger flow re-evaluation
207
+ */
208
+ export function setUser(user: DapUser): void {
209
+ userContextService.setUser(user);
210
+
211
+ // If flows were waiting for user context, start them now
212
+ if (_pendingFlowIds.length > 0 && !_flowInitializationPending) {
213
+ log("User context set - starting pending flows");
214
+ initializeFlowsWhenReady();
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Update existing user context
220
+ */
221
+ export function updateUser(partialUser: Partial<DapUser>): void {
222
+ userContextService.updateUser(partialUser);
223
+ }
224
+
225
+ /**
226
+ * Get current user context
227
+ */
228
+ export function getUser(): DapUser | null {
229
+ return userContextService.getUser();
230
+ }
231
+
232
+ /**
233
+ * Clear user context
234
+ */
235
+ export function clearUser(): void {
236
+ userContextService.clearUser();
237
+ }
238
+
239
+ function joinUrl(base: string, path: string): string {
240
+ return base.replace(/\/$/, '') + '/' + path.replace(/^\//, '');
241
+ }
242
+
243
+ import { resolveSelector } from './utils/selectors';
244
+
245
+ // Minimal API for backwards compatibility
246
+ export function runModalSequence() {
247
+ console.warn("[DAP] runModalSequence is deprecated, flows are managed by FlowEngine");
248
+ }
249
+
250
+ // New API for executing custom flows (useful for testing and dynamic flows)
251
+ export async function executeFlow(flow: any): Promise<void> {
252
+ if (!flow || !flow.id || !flow.steps) {
253
+ throw new Error("Invalid flow object: must have 'id' and 'steps' properties");
254
+ }
255
+
256
+ // Normalize the flow from test format to FlowData format
257
+ const normalizedFlow: FlowData = {
258
+ flowId: flow.id,
259
+ flowName: flow.name || flow.id,
260
+ steps: flow.steps.map((step: any, index: number) => ({
261
+ stepId: step.id || `step-${index + 1}`,
262
+ stepOrder: index + 1,
263
+ uxExperience: {
264
+ uxExperienceType: step.type,
265
+ elementSelector: step.trigger?.selector,
266
+ elementTrigger: step.trigger?.type || 'immediate',
267
+ elementLocation: step.trigger?.placement || 'auto',
268
+ content: step.content
269
+ }
270
+ }))
271
+ };
272
+
273
+ log("Executing custom flow:", normalizedFlow);
274
+ return flowEngine.startFlow(normalizedFlow);
275
+ }
276
+
277
+ /**
278
+ * Register a flow for later execution by ID
279
+ */
280
+ export function registerFlow(flowData: FlowData): void {
281
+ _registeredFlows.set(flowData.flowId, flowData);
282
+ log("Flow registered:", flowData.flowId);
283
+ }
284
+
285
+ /**
286
+ * Start a flow by ID (from registered flows or backend)
287
+ */
288
+ export async function startFlow(flowId: string): Promise<void> {
289
+ // First check if flow is registered in memory
290
+ const registeredFlow = _registeredFlows.get(flowId);
291
+ if (registeredFlow) {
292
+ log("Starting registered flow:", flowId);
293
+ return flowEngine.startFlow(registeredFlow);
294
+ }
295
+
296
+ // If not found in memory, try to fetch from backend
297
+ if (!_dapConfig) {
298
+ throw new Error("SDK not initialized. Call init() first or register the flow using registerFlow()");
299
+ }
300
+
301
+ try {
302
+ const rawFlowData = await fetchFlowById(_dapConfig, location.origin, flowId);
303
+ const flowData: FlowData = {
304
+ flowId: rawFlowData.flowId,
305
+ flowName: rawFlowData.flowName || flowId,
306
+ steps: rawFlowData.steps || []
307
+ };
308
+ log("Starting flow from backend:", flowId);
309
+ return flowEngine.startFlow(flowData);
310
+ } catch (error) {
311
+ throw new Error(`Flow not found: ${flowId}. Make sure to register it first or check if it exists in the backend.`);
312
+ }
313
+ }
314
+
315
+ // Expose core services for debugging
316
+ if (typeof window !== "undefined") {
317
+ (window as any).DAP = {
318
+ init,
319
+ executeFlow, // Add executeFlow to public API
320
+ registerFlow, // Add registerFlow to public API
321
+ startFlow, // Add startFlow to public API
322
+ // User context APIs
323
+ setUser,
324
+ updateUser,
325
+ getUser,
326
+ clearUser,
327
+ // Core services
328
+ locationContext: LocationContextService.getInstance(),
329
+ userContext: userContextService,
330
+ flowEngine,
331
+ // Debug methods
332
+ getFlowState: () => flowEngine.getState(),
333
+ getUserState: () => userContextService.getDebugState(),
334
+ resolveSelector, // Expose for testing
335
+ // Development utilities (only in debug mode)
336
+ ...(typeof (window as any).__DAP_DEBUG__ !== 'undefined' && (window as any).__DAP_DEBUG__ ? {
337
+ testFlow: async (flowId: string) => {
338
+ if (!_dapConfig) throw new Error("SDK not initialized");
339
+ const rawFlowData = await fetchFlowById(_dapConfig, location.origin, flowId);
340
+ const flowData: FlowData = {
341
+ flowId: rawFlowData.flowId,
342
+ flowName: rawFlowData.flowName || flowId,
343
+ steps: rawFlowData.steps || []
344
+ };
345
+ return flowEngine.startFlow(flowData);
346
+ },
347
+ renderModal // Add for testing
348
+ } : {
349
+ renderModal // Always available for testing draggable functionality
350
+ })
351
+ };
352
+ }
353
+
354
+ // Export types for external use
355
+ export type { DapUser } from "./services/userContextService";