@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.
- package/.github/copilot-instructions.md +95 -0
- package/README.md +79 -0
- package/TRACKING.md +105 -0
- package/USER_CONTEXT_README.md +284 -0
- package/package.json +154 -0
- package/src/config.ts +25 -0
- package/src/core/flowEngine.ts +1833 -0
- package/src/core/triggerManager.ts +1011 -0
- package/src/experiences/banner.ts +366 -0
- package/src/experiences/beacon.ts +668 -0
- package/src/experiences/hotspotTour.ts +654 -0
- package/src/experiences/hotspots.ts +566 -0
- package/src/experiences/modal.ts +1337 -0
- package/src/experiences/modalSequence.ts +1247 -0
- package/src/experiences/popover.ts +652 -0
- package/src/experiences/registry.ts +21 -0
- package/src/experiences/survey.ts +1639 -0
- package/src/experiences/taskList.ts +625 -0
- package/src/experiences/tooltip.ts +740 -0
- package/src/experiences/types.ts +395 -0
- package/src/experiences/walkthrough.ts +670 -0
- package/src/flow-sequence.ts +177 -0
- package/src/flows.ts +512 -0
- package/src/http.ts +61 -0
- package/src/index.ts +355 -0
- package/src/services/flowManager.ts +905 -0
- package/src/services/flowNormalizer.ts +74 -0
- package/src/services/locationContextService.ts +189 -0
- package/src/services/pageContextService.ts +221 -0
- package/src/services/userContextService.ts +286 -0
- package/src/state/appState.ts +0 -0
- package/src/state/hooks.ts +0 -0
- package/src/state/index.ts +0 -0
- package/src/state/migration.ts +0 -0
- package/src/state/store.ts +0 -0
- package/src/styles/banner.css.ts +0 -0
- package/src/styles/hotspot.css.ts +0 -0
- package/src/styles/hotspotTour.css.ts +0 -0
- package/src/styles/modal.css.ts +564 -0
- package/src/styles/survey.css.ts +1013 -0
- package/src/styles/taskList.css.ts +0 -0
- package/src/styles/tooltip.css.ts +149 -0
- package/src/styles/walkthrough.css.ts +0 -0
- package/src/tourUtils.ts +0 -0
- package/src/tracking.ts +223 -0
- package/src/utils/debounce.ts +66 -0
- package/src/utils/eventSequenceValidator.ts +124 -0
- package/src/utils/flowTrackingSystem.ts +524 -0
- package/src/utils/idGenerator.ts +155 -0
- package/src/utils/immediateValidationPrevention.ts +184 -0
- package/src/utils/normalize.ts +50 -0
- package/src/utils/privacyManager.ts +166 -0
- package/src/utils/ruleEvaluator.ts +199 -0
- package/src/utils/sanitize.ts +79 -0
- package/src/utils/selectors.ts +107 -0
- package/src/utils/stepExecutor.ts +345 -0
- package/src/utils/triggerNormalizer.ts +149 -0
- package/src/utils/validationInterceptor.ts +650 -0
- package/tsconfig.json +13 -0
- 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
|
+
}
|