@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
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";
|