@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,177 @@
1
+ import { DapConfig } from './config';
2
+ import { TrackingAction } from "./utils/flowTrackingSystem";
3
+
4
+ /**
5
+ * Interface representing flow context information
6
+ */
7
+ export interface FlowContext {
8
+ /** The previous flow ID in a sequence */
9
+ previousFlowId: string;
10
+ /** The full sequence of flow IDs including the current one */
11
+ sequence: string[];
12
+ /** How the flow sequence was started */
13
+ entryPoint: string;
14
+ /** The step ID that triggered the transition to this flow */
15
+ referringStepId: string;
16
+ /** Timestamp when the sequence started */
17
+ startTime: number;
18
+ /** Total number of interactions across the flow sequence */
19
+ totalInteractions: number;
20
+ }
21
+
22
+ // Store active flow contexts by flow ID
23
+ const activeFlowContexts = new Map<string, FlowContext>();
24
+
25
+ /**
26
+ * Save the current flow context for transition to another flow
27
+ * @param currentFlowId - The ID of the current flow
28
+ * @param targetFlowId - The ID of the flow we're transitioning to
29
+ * @param referringStepId - The step ID that triggered the transition
30
+ */
31
+ export function saveFlowTransitionContext(
32
+ currentFlowId: string,
33
+ targetFlowId: string,
34
+ referringStepId: string
35
+ ): void {
36
+ // Get existing context or create new one
37
+ const existingContext = activeFlowContexts.get(currentFlowId) || {
38
+ previousFlowId: "",
39
+ sequence: [currentFlowId],
40
+ entryPoint: "direct",
41
+ referringStepId: "",
42
+ startTime: Date.now(),
43
+ totalInteractions: 0
44
+ };
45
+
46
+ // Create context for the target flow
47
+ const targetContext: FlowContext = {
48
+ previousFlowId: currentFlowId,
49
+ sequence: [...existingContext.sequence, targetFlowId],
50
+ entryPoint: "flow_transition",
51
+ referringStepId: referringStepId,
52
+ startTime: existingContext.startTime, // Maintain original start time
53
+ totalInteractions: existingContext.totalInteractions
54
+ };
55
+
56
+ // Store the context
57
+ activeFlowContexts.set(targetFlowId, targetContext);
58
+
59
+ // Store in sessionStorage to persist across page loads
60
+ try {
61
+ sessionStorage.setItem(
62
+ `dap_flow_context_${targetFlowId}`,
63
+ JSON.stringify(targetContext)
64
+ );
65
+ // Also store the last active flow ID for easy retrieval
66
+ sessionStorage.setItem('dap_last_active_flow', currentFlowId);
67
+ sessionStorage.setItem('dap_next_flow', targetFlowId);
68
+ } catch (e) {
69
+ console.error("[DAP] Failed to store flow context:", e);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get flow context for a flow (if available)
75
+ * @param flowId - The ID of the flow
76
+ */
77
+ export function getFlowContext(flowId: string): FlowContext | null {
78
+ // First check memory
79
+ if (activeFlowContexts.has(flowId)) {
80
+ return activeFlowContexts.get(flowId)!;
81
+ }
82
+
83
+ // Then check sessionStorage
84
+ try {
85
+ const storedContext = sessionStorage.getItem(`dap_flow_context_${flowId}`);
86
+ if (storedContext) {
87
+ const context = JSON.parse(storedContext);
88
+ activeFlowContexts.set(flowId, context);
89
+ return context;
90
+ }
91
+ } catch (e) {
92
+ console.error("[DAP] Failed to retrieve flow context:", e);
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Track transition between flows
100
+ * @param config - DAP configuration
101
+ * @param fromFlowId - Source flow ID
102
+ * @param toFlowId - Target flow ID
103
+ * @param stepId - The step ID that triggered the transition
104
+ */
105
+ export function trackFlowTransition(
106
+ config: DapConfig,
107
+ fromFlowId: string,
108
+ toFlowId: string,
109
+ stepId: string
110
+ ): void {
111
+ console.debug(`[DAP] Tracking flow transition from ${fromFlowId} to ${toFlowId} from step ${stepId}`);
112
+
113
+ // Save context for the next flow
114
+ saveFlowTransitionContext(fromFlowId, toFlowId, stepId);
115
+
116
+ // Track the transition event - legacy, now handled by step view system
117
+ console.debug('[DAP] Flow transition tracking - handled by step view system');
118
+ }
119
+
120
+ /**
121
+ * Track completion of a flow sequence
122
+ * @param config - DAP configuration
123
+ * @param flowId - Current flow ID
124
+ * @param context - Flow context information
125
+ */
126
+ export function trackSequenceCompletion(
127
+ config: DapConfig,
128
+ flowId: string,
129
+ context: FlowContext
130
+ ): void {
131
+ const now = Date.now();
132
+ const sequenceTotalTime = now - context.startTime;
133
+
134
+ console.debug(`[DAP] Tracking sequence completion for flows: ${context.sequence.join(' → ')}`);
135
+
136
+ // Legacy: Sequence completion tracking - now handled by step view system
137
+ console.debug('[DAP] Flow sequence completion tracking - handled by step view system');
138
+ }
139
+
140
+ /**
141
+ * Check if the current navigation appears to be part of a flow transition
142
+ * (to be called during page load)
143
+ */
144
+ export function checkForFlowTransition(): { fromFlow: string, toFlow: string } | null {
145
+ try {
146
+ // Check if we have a stored transition
147
+ const lastActiveFlow = sessionStorage.getItem('dap_last_active_flow');
148
+ const nextFlow = sessionStorage.getItem('dap_next_flow');
149
+
150
+ if (lastActiveFlow && nextFlow) {
151
+ // Clear the transition markers (they're one-time use)
152
+ sessionStorage.removeItem('dap_last_active_flow');
153
+ sessionStorage.removeItem('dap_next_flow');
154
+
155
+ return {
156
+ fromFlow: lastActiveFlow,
157
+ toFlow: nextFlow
158
+ };
159
+ }
160
+
161
+ // Check URL parameters for flow transition info
162
+ const urlParams = new URLSearchParams(window.location.search);
163
+ const fromFlowParam = urlParams.get('from_flow');
164
+ const toFlowParam = urlParams.get('to_flow') || urlParams.get('flow');
165
+
166
+ if (fromFlowParam && toFlowParam) {
167
+ return {
168
+ fromFlow: fromFlowParam,
169
+ toFlow: toFlowParam
170
+ };
171
+ }
172
+ } catch (e) {
173
+ console.error("[DAP] Error checking for flow transition:", e);
174
+ }
175
+
176
+ return null;
177
+ }
package/src/flows.ts ADDED
@@ -0,0 +1,512 @@
1
+ // src/flows.ts
2
+ // Transport + server → client normalization (Modal, Tooltip, Popover, Knowledge Base, Survey)
3
+
4
+ import type {
5
+ ModalContent,
6
+ ModalSequencePayload,
7
+ ModalSequenceStep,
8
+ TooltipPayload,
9
+ PopoverPayload,
10
+ KBItem,
11
+ KBItemType,
12
+ ModalKB,
13
+ } from "./experiences/types";
14
+ import type { SurveyPayload, SurveyQuestion } from "./experiences/survey";
15
+ import { flowManager, type ContextualFlow } from "./services/flowManager";
16
+ import { normalizeFlows, normalizeFlow as normalizeServerToContextualFlow } from "./services/flowNormalizer";
17
+ import { normalizePlacement } from "./utils/normalize";
18
+ import { normalizeTrigger as advancedNormalizeTrigger } from "./utils/triggerNormalizer";
19
+ import { normalizeStepId } from "./utils/idGenerator";
20
+
21
+ /**
22
+ * Convert advanced trigger normalization to legacy format for backward compatibility
23
+ * This ensures existing payload types work while supporting enhanced trigger events
24
+ */
25
+ function normalizeTriggerToLegacyFormat(trigger: string | undefined): "hover" | "focus" | "click" {
26
+ const normalized = advancedNormalizeTrigger(trigger);
27
+
28
+ // Map new event types to legacy format for type compatibility
29
+ switch (normalized.eventType) {
30
+ case 'mouseenter':
31
+ case 'hover':
32
+ return 'hover';
33
+ case 'focus':
34
+ case 'input':
35
+ case 'change':
36
+ return 'focus';
37
+ case 'click':
38
+ case 'keydown':
39
+ case 'keyup':
40
+ default:
41
+ return 'click';
42
+ }
43
+ }
44
+
45
+ import { http } from "./http";
46
+ import type { DapConfig } from "./config";
47
+
48
+ /* ------------ Public API used by index.ts ------------ */
49
+
50
+ export async function fetchVisibleFlowIds(cfg: DapConfig, hostBase: string, page: string | null): Promise<string[]> {
51
+ const base = joinUrl(cfg.apiurl, `/iap-experience/${cfg.organizationid}/${cfg.siteid}/visible-flows`);
52
+
53
+ // Try POST with JSON body (works in demo/mock). If server rejects (405), retry GET with ?hostname=...
54
+ try {
55
+ const res = await http(cfg, base, {
56
+ method: "POST",
57
+ hostBase,
58
+ includeHostHeader: true,
59
+ body: { hostname: hostBase, page: null },
60
+ });
61
+ return Array.isArray(res) ? (res as string[]) : [];
62
+ } catch (e: any) {
63
+ if (e && e.status === 405) {
64
+ const url = `${base}?hostname=${encodeURIComponent(hostBase)}`;
65
+ const res = await http(cfg, url, {
66
+ method: "GET",
67
+ hostBase,
68
+ includeHostHeader: true,
69
+ });
70
+ return Array.isArray(res?.flowIds) ? (res.flowIds as string[]) : [];
71
+ }
72
+ throw e;
73
+ }
74
+ }
75
+
76
+ export async function fetchFlowById(cfg: DapConfig, hostBase: string, flowId: string): Promise<any> {
77
+ const url = joinUrl(cfg.apiurl, `/iap-experience/${cfg.organizationid}/${cfg.siteid}/flows/${flowId}`);
78
+ return http(cfg, url, { method: "GET", hostBase, includeHostHeader: true });
79
+ }
80
+
81
+ /** Normalize a raw server flow JSON to a client-side modalSequence payload */
82
+ export function normalizeServerFlow(serverFlow: any): ModalSequencePayload {
83
+ console.debug("[DAP] === NORMALIZING SERVER FLOW ===");
84
+ console.debug("[DAP] Raw server flow data:", serverFlow);
85
+ console.debug("[DAP] Flow ID:", serverFlow?.flowId);
86
+ console.debug("[DAP] Flow Name:", serverFlow?.flowName);
87
+ console.debug("[DAP] Steps count:", serverFlow?.steps?.length);
88
+
89
+ // Get contextual properties from the first step for flow manager
90
+ let hasContextualProperties = false;
91
+ let elementSelector: string | undefined;
92
+ let elementTrigger: string | undefined;
93
+ let elementLocation: string | undefined;
94
+
95
+ const firstStep = serverFlow?.steps?.[0];
96
+ if (firstStep?.uxExperience) {
97
+ const ux = firstStep.uxExperience;
98
+ elementSelector = ux.elementSelector;
99
+ elementTrigger = ux.elementTrigger;
100
+ elementLocation = ux.elementLocation;
101
+
102
+ hasContextualProperties = !!(elementSelector || elementLocation);
103
+ }
104
+
105
+ // Standard payload for modal sequence
106
+ const out: ModalSequencePayload = { steps: [], startAt: 0 };
107
+
108
+ const steps = Array.isArray(serverFlow?.steps) ? serverFlow.steps : [];
109
+ console.debug(`[DAP] Processing flow with ${steps.length} steps`);
110
+
111
+ for (const step of steps) {
112
+ console.debug(`[DAP] Processing step:`, {
113
+ stepId: step?.stepId,
114
+ stepName: step?.stepName,
115
+ hasUxExperience: !!step?.uxExperience,
116
+ hasConditionRuleBlocks: !!(step?.conditionRuleBlocks && step.conditionRuleBlocks.length > 0),
117
+ conditionRuleBlocksLength: step?.conditionRuleBlocks?.length || 0
118
+ });
119
+
120
+ // -------- Rule Step (conditionRuleBlocks) --------
121
+ // Check if this step has conditionRuleBlocks and should be a rule step
122
+ if (step?.conditionRuleBlocks && Array.isArray(step.conditionRuleBlocks) && step.conditionRuleBlocks.length > 0) {
123
+ console.debug(`[DAP] Processing rule step:`, step);
124
+
125
+ const stepId = normalizeStepId(step.stepId, "step", out.steps.length + 1);
126
+
127
+ // Use userInputSelector if available, otherwise use selector from first rule block
128
+ const inputSelector = step.userInputSelector ||
129
+ (step.conditionRuleBlocks[0]?.selector) || "";
130
+
131
+ console.debug(`[DAP] Rule step input selector: ${inputSelector}`);
132
+ console.debug(`[DAP] Rule step has ${step.conditionRuleBlocks.length} rule blocks`);
133
+
134
+ if (!inputSelector) {
135
+ console.warn(`[DAP] Rule step ${step.stepId} has no input selector, skipping`);
136
+ continue;
137
+ }
138
+
139
+ const ruleStep = {
140
+ kind: "rule" as const,
141
+ stepId: stepId,
142
+ inputSelector: inputSelector,
143
+ rules: step.conditionRuleBlocks
144
+ };
145
+
146
+ console.debug(`[DAP] Created rule step:`, ruleStep);
147
+
148
+ out.steps.push({
149
+ kind: "rule",
150
+ rule: ruleStep,
151
+ stepId: stepId
152
+ });
153
+
154
+ console.debug(`[DAP] Added rule step to modal sequence. Total steps: ${out.steps.length}`);
155
+ continue;
156
+ }
157
+
158
+ const ux = step?.uxExperience;
159
+ if (!ux) {
160
+ console.debug(`[DAP] Skipping step ${step?.stepId} - no uxExperience and no conditionRuleBlocks`);
161
+ continue;
162
+ }
163
+
164
+ const uxType = String(ux.uxExperienceType || "").toLowerCase();
165
+
166
+ // -------- Tooltip --------
167
+ if (uxType === "tooltip" || ux?.content?.componentType === "Tooltip") {
168
+ // Always normalize and guarantee uniqueness
169
+ const stepId = normalizeStepId(step.stepId, "step", out.steps.length + 1);
170
+ const t: TooltipPayload = {
171
+ targetSelector: ux.elementSelector || "",
172
+ text: ux?.content?.text || "",
173
+ placement: normalizePlacement(ux?.content?.placement),
174
+ trigger: normalizeTriggerToLegacyFormat(ux.elementTrigger),
175
+ stepId: stepId
176
+ };
177
+ out.steps.push({
178
+ kind: "tooltip",
179
+ tooltip: t,
180
+ title: ux?.name || "Tip",
181
+ stepId: stepId,
182
+ // Preserve trigger information at step level
183
+ elementSelector: ux.elementSelector,
184
+ elementTrigger: ux.elementTrigger,
185
+ elementLocation: ux.elementLocation
186
+ });
187
+ continue;
188
+ }
189
+
190
+ // -------- Popover --------
191
+ if (uxType === "popover" || ux?.content?.componentType === "Popover") {
192
+ const stepId = normalizeStepId(step.stepId, "step", out.steps.length + 1);
193
+ const p: PopoverPayload = {
194
+ title: ux?.content?.title || ux?.name || "Info",
195
+ body: ux?.content?.body || "",
196
+ bodyBlocks: Array.isArray(ux?.content?.bodyBlocks) ? ux.content.bodyBlocks : undefined,
197
+ targetSelector: ux?.elementSelector || "",
198
+ placement: normalizePlacement(ux?.content?.placement),
199
+ trigger: normalizeTriggerToLegacyFormat(ux.elementTrigger),
200
+ showArrow: ux?.content?.showArrow !== false,
201
+ stepId: stepId
202
+ };
203
+ out.steps.push({
204
+ kind: "popover",
205
+ popover: p,
206
+ title: p.title,
207
+ stepId: stepId,
208
+ // Preserve trigger information at step level
209
+ elementSelector: ux.elementSelector,
210
+ elementTrigger: ux.elementTrigger,
211
+ elementLocation: ux.elementLocation
212
+ });
213
+ continue;
214
+ }
215
+
216
+ // -------- Modal (incl. Knowledge Base) --------
217
+ if (uxType === "modal" || ux?.content?.componentType === "Modal") {
218
+ const blocks: ModalContent[] = [];
219
+
220
+ console.debug("[DAP] Processing modal content for step:", step.stepName);
221
+ console.debug("[DAP] ux.content:", ux?.content);
222
+ console.debug("[DAP] ux.modalContent:", ux?.modalContent);
223
+
224
+ // For Knowledge Base modals, skip body text and only process KB content
225
+ const contentType = String(ux?.modalContent?.contentType || "").toLowerCase();
226
+ const isKnowledgeBase = contentType === "knowledgebase";
227
+ console.log("[DAP] Processing Knowledge Base content:", isKnowledgeBase);
228
+ console.debug("[DAP] Is Knowledge Base modal:", isKnowledgeBase);
229
+ console.debug("[DAP] modalContent.contentType:", ux?.modalContent?.contentType);
230
+ console.debug("[DAP] modalContent full object:", ux?.modalContent);
231
+ console.log("[DAP] ux.content.body:", ux?.content?.body);
232
+ if (ux?.content?.body && !isKnowledgeBase) {
233
+ console.debug("[DAP] Adding text block with body:", typeof ux.content.body, ux.content.body);
234
+
235
+ // Safety check: if body is an array (KB items), don't add it as text
236
+ if (Array.isArray(ux.content.body)) {
237
+ console.warn("[DAP] Body content is an array, likely KB items incorrectly assigned to body:", ux.content.body);
238
+ // Create KB block instead of text block
239
+ const kbItems = toKbItems(ux.content.body);
240
+ console.debug("[DAP] Creating KB block from body array:", kbItems);
241
+ const kb: ModalKB = {
242
+ kind: "kb",
243
+ title: ux?.content?.header || ux?.name || "Knowledge Base",
244
+ items: kbItems,
245
+ };
246
+ blocks.push(kb);
247
+ console.debug("[DAP] Added KB block from body array:", kb);
248
+ } else {
249
+ blocks.push({ kind: "text", html: String(ux.content.body) });
250
+ }
251
+ } else if (ux?.content?.body && isKnowledgeBase) {
252
+ console.debug("[DAP] Skipping body text for Knowledge Base modal, body content:", ux.content.body);
253
+ }
254
+
255
+ // Knowledge Base block
256
+ if (contentType === "knowledgebase") {
257
+ console.debug("[DAP] Processing Knowledge Base content:", ux?.modalContent);
258
+ const kbItems = toKbItems(ux?.modalContent?.contentData);
259
+ console.debug("[DAP] Generated KB items:", kbItems);
260
+
261
+ const kb: ModalKB = {
262
+ kind: "kb",
263
+ title: ux?.content?.header || ux?.name || "Knowledge Base",
264
+ items: kbItems,
265
+ };
266
+ blocks.push(kb);
267
+ console.debug("[DAP] Added KB block:", kb);
268
+ } else {
269
+ // Single-content modal
270
+ const c = ux?.modalContent;
271
+ if (c) {
272
+ const url = c.presignedUrl || c.contentData || "";
273
+ const ctype = String(c.contentType || "").toLowerCase();
274
+ if (ctype === "link") {
275
+ if (isYouTube(url)) {
276
+ blocks.push({ kind: "youtube", href: url, title: c.contentName || "YouTube" });
277
+ } else if (isHttp(url)) {
278
+ blocks.push({ kind: "link", href: url, label: c.contentName || url });
279
+ }
280
+ } else if (ctype === "video") {
281
+ if (isHttp(url)) blocks.push({ kind: "video", sources: [{ src: url }] });
282
+ } else if (ctype === "image") {
283
+ if (isHttp(url)) blocks.push({ kind: "image", url, alt: c.contentName || "" });
284
+ } else if (ctype === "article") {
285
+ if (isHttp(url)) {
286
+ blocks.push({
287
+ kind: "article",
288
+ url,
289
+ fileName: c.contentData || undefined,
290
+ mime: /\.pdf(\?|#|$)/i.test(url)
291
+ ? "application/pdf"
292
+ : /\.docx(\?|#|$)/i.test(url)
293
+ ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
294
+ : undefined,
295
+ });
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ console.debug("[DAP] Final blocks array for modal:", blocks);
302
+ console.debug("[DAP] Block kinds:", blocks.map(b => b.kind));
303
+
304
+ const stepId = normalizeStepId(step.stepId, "step", out.steps.length + 1);
305
+ out.steps.push({
306
+ kind: "modal",
307
+ title: ux?.content?.header || ux?.name || "Info",
308
+ footerText: ux?.content?.footer || "",
309
+ body: blocks,
310
+ stepId: stepId,
311
+ // Preserve trigger information at step level
312
+ elementSelector: ux.elementSelector,
313
+ elementTrigger: ux.elementTrigger,
314
+ elementLocation: ux.elementLocation
315
+ });
316
+ continue;
317
+ }
318
+
319
+ // -------- Survey --------
320
+ if (uxType === "survey" || ux?.content?.componentType === "MicroSurvey") {
321
+ const stepId = normalizeStepId(step.stepId, "step", out.steps.length + 1);
322
+ const survey: SurveyPayload = {
323
+ header: ux?.content?.header || ux?.name || "Survey",
324
+ body: ux?.content?.body || "",
325
+ questions: Array.isArray(ux?.content?.questions)
326
+ ? ux.content.questions.map((q: any) => ({
327
+ questionId: q.questionId || `q${Math.random().toString(36).substring(2, 10)}`,
328
+ question: q.question || "",
329
+ type: q.type || "SingleChoice",
330
+ options: Array.isArray(q.options) ? q.options : undefined,
331
+ scaleMin: q.scaleMin !== undefined ? q.scaleMin : undefined,
332
+ scaleMax: q.scaleMax !== undefined ? q.scaleMax : undefined,
333
+ labelMin: q.labelMin || undefined,
334
+ labelMax: q.labelMax || undefined,
335
+ criteria: Array.isArray(q.criteria) ? q.criteria : undefined,
336
+ } as SurveyQuestion))
337
+ : [],
338
+ flowId: serverFlow.flowId,
339
+ organizationId: serverFlow.organizationId,
340
+ siteId: serverFlow.siteId,
341
+ stepId: stepId
342
+ };
343
+ out.steps.push({
344
+ kind: "survey",
345
+ survey: survey,
346
+ title: survey.header || "Survey",
347
+ stepId: stepId,
348
+ // Preserve trigger information at step level
349
+ elementSelector: ux.elementSelector,
350
+ elementTrigger: ux.elementTrigger,
351
+ elementLocation: ux.elementLocation
352
+ });
353
+ continue;
354
+ }
355
+ }
356
+
357
+ // Normalize all step IDs using our utility
358
+ out.steps.forEach(step => {
359
+ if (step.stepId) {
360
+ step.stepId = normalizeStepId(step.stepId);
361
+
362
+ // Also update in the nested payload if present
363
+ if (step.tooltip && step.tooltip.stepId) {
364
+ step.tooltip.stepId = step.stepId;
365
+ } else if (step.popover && step.popover.stepId) {
366
+ step.popover.stepId = step.stepId;
367
+ } else if (step.survey && step.survey.stepId) {
368
+ step.survey.stepId = step.stepId;
369
+ }
370
+ }
371
+ });
372
+
373
+ // Store the total steps in the payload for tracking
374
+ if (out.steps.length > 0) {
375
+ // Add a steps-count attribute to the payload for total steps tracking
376
+ out.stepsCount = out.steps.length;
377
+ }
378
+
379
+ // All flows are now handled directly by modalSequence
380
+ // modalSequence is responsible for handling all triggers and step transitions
381
+ // FlowManager is not used for flow registration in this architecture
382
+
383
+ console.debug("[DAP] === NORMALIZATION COMPLETE ===");
384
+ console.debug("[DAP] Total steps created:", out.steps.length);
385
+ console.debug("[DAP] Final modal sequence payload:", out);
386
+ out.steps.forEach((step, index) => {
387
+ console.debug(`[DAP] Step ${index + 1}:`, {
388
+ kind: step.kind,
389
+ stepId: step.stepId,
390
+ hasRule: step.kind === 'rule',
391
+ ruleData: step.kind === 'rule' ? step.rule : undefined
392
+ });
393
+ });
394
+ console.debug("[DAP] === END NORMALIZATION ===");
395
+
396
+ return out;
397
+ }
398
+
399
+ /**
400
+ * Convert server flow to contextual flow format
401
+ */
402
+ function convertToContextualFlow(serverFlow: any): ContextualFlow {
403
+ let elementSelector: string | undefined = undefined;
404
+ let elementTrigger: string | undefined = undefined;
405
+ let elementLocation: string | undefined = undefined;
406
+
407
+ // Create an empty modal sequence payload
408
+ const payload: ModalSequencePayload = { steps: [], startAt: 0 };
409
+
410
+ // Extract contextual properties from the first step's UX experience
411
+ const steps = Array.isArray(serverFlow?.steps) ? serverFlow.steps : [];
412
+ if (steps.length > 0) {
413
+ const step = steps[0];
414
+ const ux = step?.uxExperience;
415
+
416
+ if (ux) {
417
+ elementSelector = ux.elementSelector;
418
+ elementTrigger = ux.elementTrigger;
419
+ elementLocation = ux.elementLocation;
420
+ }
421
+ }
422
+
423
+ return {
424
+ id: serverFlow.id || serverFlow.flowId || `flow-${Math.random().toString(36).substring(2, 10)}`,
425
+ type: "modalSequence",
426
+ payload,
427
+ elementSelector,
428
+ elementTrigger,
429
+ elementLocation,
430
+ originalData: serverFlow
431
+ };
432
+ }
433
+
434
+ /* ------------ helpers ------------ */
435
+
436
+ export type NormalizedFlow = ModalSequencePayload; // for index.ts legacy imports
437
+
438
+ function toTrigger(t: string | undefined | null): "hover" | "focus" | "click" {
439
+ // Use the enhanced trigger normalization with backward compatibility
440
+ return normalizeTriggerToLegacyFormat(t || undefined);
441
+ }
442
+
443
+ function isHttp(url: string): boolean {
444
+ try { const u = new URL(url, location.origin); return /^https?:$/i.test(u.protocol); } catch { return false; }
445
+ }
446
+
447
+ function isYouTube(url: string): boolean {
448
+ try {
449
+ const u = new URL(url, location.origin);
450
+ const h = u.hostname.toLowerCase();
451
+ return /(^|\.)youtube\.com$/.test(h) || /(^|\.)youtu\.be$/.test(h) || /(^|\.)youtube-nocookie\.com$/.test(h);
452
+ } catch { return false; }
453
+ }
454
+
455
+ function mapItemType(t: string, url: string): KBItemType {
456
+ const v = (t || "").toLowerCase();
457
+ if (v === "link") return isYouTube(url) ? "youtube" : "link";
458
+ if (v === "video") return "video";
459
+ if (v === "image") return "image";
460
+ if (v === "article") return "article";
461
+ return "link";
462
+ }
463
+
464
+ function toKbItems(arr: any[] | null | undefined): KBItem[] {
465
+ console.debug("[DAP] toKbItems input:", arr);
466
+ if (!Array.isArray(arr)) return [];
467
+ const items: KBItem[] = [];
468
+ for (const it of arr) {
469
+ // Extract data from the KB item structure from flow.json
470
+ const url = it?.presignedUrl || "";
471
+ const title = it?.contentName || "";
472
+ const description = it?.contentDescription || "";
473
+ const contentType = it?.contentType || "";
474
+ const fileName = it?.contentData || "";
475
+
476
+ console.debug("[DAP] Processing KB item:", {
477
+ raw: it,
478
+ extracted: { url, title, description, contentType, fileName }
479
+ });
480
+
481
+ if (!url || !title) {
482
+ console.warn("[DAP] KB item missing required fields (url or title), skipping:", it);
483
+ continue;
484
+ }
485
+
486
+ const kbItem = {
487
+ kind: "kb-item" as const,
488
+ itemType: mapItemType(contentType, url),
489
+ title,
490
+ description,
491
+ url,
492
+ fileName: fileName || undefined,
493
+ mime:
494
+ /\.pdf(\?|#|$)/i.test(url)
495
+ ? "application/pdf"
496
+ : /\.docx(\?|#|$)/i.test(url)
497
+ ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
498
+ : undefined,
499
+ };
500
+ console.debug("[DAP] Generated KB item:", kbItem);
501
+ items.push(kbItem);
502
+ }
503
+ console.debug("[DAP] Final KB items array:", items);
504
+ return items;
505
+ }
506
+
507
+ /** Join URLs safely: trims duplicate slashes */
508
+ function joinUrl(base: string, tail: string): string {
509
+ const b = (base || "").replace(/\/+$/, "");
510
+ const t = (tail || "").replace(/^\/+/, "");
511
+ return `${b}/${t}`;
512
+ }