@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,74 @@
1
+ // src/services/flowNormalizer.ts
2
+ // Service for normalizing flow data from server format to client format
3
+
4
+ import type { ContextualFlow } from './flowManager';
5
+
6
+ /**
7
+ * Server flow format types
8
+ */
9
+ interface ServerFlow {
10
+ id: string;
11
+ type: string;
12
+ data: any;
13
+ context?: {
14
+ elementSelector?: string;
15
+ elementTrigger?: string;
16
+ elementLocation?: string;
17
+ [key: string]: any;
18
+ };
19
+ [key: string]: any;
20
+ }
21
+
22
+ /**
23
+ * Normalizes server flow data to the client-side format
24
+ * @param serverFlows Array of flows in server format
25
+ * @returns Array of normalized flows
26
+ */
27
+ export function normalizeFlows(serverFlows: ServerFlow[]): ContextualFlow[] {
28
+ return serverFlows.map(serverFlow => {
29
+ const baseFlow = {
30
+ id: serverFlow.id,
31
+ type: serverFlow.type.toLowerCase(),
32
+ payload: serverFlow.data || {},
33
+ originalData: serverFlow
34
+ };
35
+
36
+ // Add contextual properties if available
37
+ if (serverFlow.context) {
38
+ return {
39
+ ...baseFlow,
40
+ elementSelector: serverFlow.context.elementSelector,
41
+ elementTrigger: serverFlow.context.elementTrigger || 'click',
42
+ elementLocation: serverFlow.context.elementLocation
43
+ };
44
+ }
45
+
46
+ return baseFlow;
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Normalizes a single server flow to client format
52
+ * @param serverFlow Flow in server format
53
+ * @returns Normalized flow
54
+ */
55
+ export function normalizeFlow(serverFlow: ServerFlow): ContextualFlow {
56
+ const baseFlow = {
57
+ id: serverFlow.id,
58
+ type: serverFlow.type.toLowerCase(),
59
+ payload: serverFlow.data || {},
60
+ originalData: serverFlow
61
+ };
62
+
63
+ // Add contextual properties if available
64
+ if (serverFlow.context) {
65
+ return {
66
+ ...baseFlow,
67
+ elementSelector: serverFlow.context.elementSelector,
68
+ elementTrigger: serverFlow.context.elementTrigger || 'click',
69
+ elementLocation: serverFlow.context.elementLocation
70
+ };
71
+ }
72
+
73
+ return baseFlow;
74
+ }
@@ -0,0 +1,189 @@
1
+ // src/services/locationContextService.ts
2
+ // Service to track the current screen/location context
3
+
4
+ export interface LocationContext {
5
+ currentPath: string;
6
+ screenId?: string;
7
+ }
8
+
9
+ /**
10
+ * Service for managing the current location context within the application.
11
+ * Tracks the current page path and screen ID to enable contextual experiences.
12
+ */
13
+ export class LocationContextService {
14
+ private static _instance: LocationContextService;
15
+ private _currentContext: LocationContext;
16
+ private _listeners: Set<(context: LocationContext) => void> = new Set();
17
+
18
+ private constructor() {
19
+ // Initialize with normalized path (remove leading slashes)
20
+ this._currentContext = {
21
+ currentPath: window.location.pathname.replace(/^\/+/, '')
22
+ };
23
+
24
+ // Listen for navigation events
25
+ window.addEventListener('popstate', this.updateContext.bind(this));
26
+ window.addEventListener('hashchange', this.updateContext.bind(this));
27
+
28
+ // For SPAs, hook into history API
29
+ this.monitorHistoryChanges();
30
+ }
31
+
32
+ /**
33
+ * Get the singleton instance of the LocationContextService
34
+ */
35
+ public static getInstance(): LocationContextService {
36
+ if (!this._instance) {
37
+ this._instance = new LocationContextService();
38
+ }
39
+ return this._instance;
40
+ }
41
+
42
+ /**
43
+ * Set the current screen ID
44
+ * @param screenId The ID of the current screen/view
45
+ */
46
+ public setScreenId(screenId: string): void {
47
+ // Normalize screenId by removing leading forward slashes
48
+ const normalizedScreenId = screenId.replace(/^\/+/, '');
49
+
50
+ this._currentContext = {
51
+ ...this._currentContext,
52
+ screenId: normalizedScreenId
53
+ };
54
+ this.notifyListeners();
55
+ }
56
+
57
+ /**
58
+ * Set the current location context
59
+ * @param context New context values
60
+ */
61
+ public setContext(context: Partial<LocationContext>): void {
62
+ // Create normalized context by removing leading slashes
63
+ const normalizedContext: Partial<LocationContext> = { ...context };
64
+
65
+ if (normalizedContext.currentPath) {
66
+ normalizedContext.currentPath = normalizedContext.currentPath.replace(/^\/+/, '');
67
+ }
68
+
69
+ if (normalizedContext.screenId) {
70
+ normalizedContext.screenId = normalizedContext.screenId.replace(/^\/+/, '');
71
+ }
72
+
73
+ this._currentContext = {
74
+ ...this._currentContext,
75
+ ...normalizedContext
76
+ };
77
+ this.notifyListeners();
78
+ }
79
+
80
+ /**
81
+ * Get the current location context
82
+ * @returns The current location context
83
+ */
84
+ public getContext(): LocationContext {
85
+ return { ...this._currentContext };
86
+ }
87
+
88
+ /**
89
+ * Subscribe to location context changes
90
+ * @param listener Function to call when the context changes
91
+ * @returns Function to unsubscribe
92
+ */
93
+ public subscribe(listener: (context: LocationContext) => void): () => void {
94
+ this._listeners.add(listener);
95
+ return () => this._listeners.delete(listener);
96
+ }
97
+
98
+ /**
99
+ * Monitor history API changes for SPAs
100
+ * This helps detect navigation in single-page applications
101
+ */
102
+ private monitorHistoryChanges(): void {
103
+ // Store original methods
104
+ const originalPushState = history.pushState;
105
+ const originalReplaceState = history.replaceState;
106
+
107
+ // Override pushState
108
+ history.pushState = (...args) => {
109
+ originalPushState.apply(history, args);
110
+ this.updateContext();
111
+ };
112
+
113
+ // Override replaceState
114
+ history.replaceState = (...args) => {
115
+ originalReplaceState.apply(history, args);
116
+ this.updateContext();
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Update the current context based on window location
122
+ */
123
+ private updateContext(): void {
124
+ // Normalize pathname by removing leading forward slashes
125
+ const normalizedPath = window.location.pathname.replace(/^\/+/, '');
126
+
127
+ this._currentContext = {
128
+ ...this._currentContext,
129
+ currentPath: normalizedPath
130
+ };
131
+ this.notifyListeners();
132
+ }
133
+
134
+ /**
135
+ * Check if the current location matches a specific location requirement
136
+ * @param elementLocation The required location/route from the flow
137
+ * @returns True if the current location matches the requirement
138
+ */
139
+ public matchesLocation(elementLocation: string): boolean {
140
+ if (!elementLocation) return true; // No requirement means match all
141
+
142
+ const currentPath = this._currentContext.currentPath || '';
143
+ const currentScreenId = this._currentContext.screenId || '';
144
+
145
+ // Normalize the required location
146
+ const normalizedRequired = elementLocation.replace(/^\/+/, '').toLowerCase();
147
+
148
+ // Check exact matches
149
+ if (currentPath.toLowerCase() === normalizedRequired ||
150
+ currentScreenId.toLowerCase() === normalizedRequired) {
151
+ return true;
152
+ }
153
+
154
+ // Check if current path contains the required location
155
+ if (currentPath.toLowerCase().includes(normalizedRequired)) {
156
+ return true;
157
+ }
158
+
159
+ // Check if current path starts with the required location (for nested routes)
160
+ if (currentPath.toLowerCase().startsWith(normalizedRequired + '/')) {
161
+ return true;
162
+ }
163
+
164
+ // Check if URL hash contains the location
165
+ const hash = window.location.hash.replace(/^#+/, '').toLowerCase();
166
+ if (hash === normalizedRequired || hash.includes(normalizedRequired)) {
167
+ return true;
168
+ }
169
+
170
+ // Check if URL search params contain route indicators
171
+ const urlParams = new URLSearchParams(window.location.search);
172
+ const routeParam = urlParams.get('route') || urlParams.get('page') || urlParams.get('view');
173
+ if (routeParam && routeParam.toLowerCase() === normalizedRequired) {
174
+ return true;
175
+ }
176
+
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Notify all listeners of context change
182
+ */
183
+ private notifyListeners(): void {
184
+ this._listeners.forEach(listener => listener(this.getContext()));
185
+ }
186
+ }
187
+
188
+ // Export singleton instance
189
+ export const locationContext = LocationContextService.getInstance();
@@ -0,0 +1,221 @@
1
+ // src/services/pageContextService.ts
2
+ // Page context management for SPA and multi-page applications
3
+
4
+ export interface PageContext {
5
+ href: string;
6
+ pathname: string;
7
+ hash: string;
8
+ search: string;
9
+ timestamp: number;
10
+ }
11
+
12
+ export interface PageChangeEvent {
13
+ previous: PageContext | null;
14
+ current: PageContext;
15
+ type: 'initial' | 'navigation' | 'popstate' | 'reload';
16
+ }
17
+
18
+ type PageChangeHandler = (event: PageChangeEvent) => void;
19
+
20
+ export class PageContextService {
21
+ private static instance: PageContextService | null = null;
22
+ private handlers: Set<PageChangeHandler> = new Set();
23
+ private currentContext: PageContext | null = null;
24
+ private initialized = false;
25
+
26
+ private constructor() {}
27
+
28
+ static getInstance(): PageContextService {
29
+ if (!PageContextService.instance) {
30
+ PageContextService.instance = new PageContextService();
31
+ }
32
+ return PageContextService.instance;
33
+ }
34
+
35
+ /**
36
+ * Initialize page context tracking
37
+ */
38
+ initialize(): void {
39
+ if (this.initialized) return;
40
+
41
+ console.debug('[DAP] PageContextService: Initializing...');
42
+
43
+ // Set initial context
44
+ this.currentContext = this.captureCurrentContext();
45
+
46
+ // Listen for browser navigation
47
+ window.addEventListener('popstate', this.handlePopState.bind(this));
48
+
49
+ // Hook into History API for SPA navigation
50
+ this.interceptHistoryMethods();
51
+
52
+ this.initialized = true;
53
+ console.debug('[DAP] PageContextService: Initialized with context:', this.currentContext);
54
+
55
+ // Notify initial page load
56
+ this.notifyHandlers({
57
+ previous: null,
58
+ current: this.currentContext,
59
+ type: 'initial'
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Get current page context
65
+ */
66
+ getCurrentContext(): PageContext {
67
+ if (!this.currentContext) {
68
+ this.currentContext = this.captureCurrentContext();
69
+ }
70
+ return this.currentContext;
71
+ }
72
+
73
+ /**
74
+ * Subscribe to page change events
75
+ */
76
+ subscribe(handler: PageChangeHandler): () => void {
77
+ this.handlers.add(handler);
78
+ return () => {
79
+ this.handlers.delete(handler);
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Manually trigger page context update (for framework integration)
85
+ */
86
+ updateContext(type: 'navigation' | 'reload' = 'navigation'): void {
87
+ const previous = this.currentContext;
88
+ const current = this.captureCurrentContext();
89
+
90
+ if (!this.hasContextChanged(previous, current)) {
91
+ return;
92
+ }
93
+
94
+ console.debug('[DAP] PageContextService: Context changed:', {
95
+ from: previous?.pathname,
96
+ to: current.pathname
97
+ });
98
+
99
+ this.currentContext = current;
100
+ this.notifyHandlers({
101
+ previous,
102
+ current,
103
+ type
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Check if selector is relevant for current page
109
+ */
110
+ isSelectorRelevant(selector: string): boolean {
111
+ if (!selector) return false;
112
+
113
+ // For now, we don't filter selectors by page context
114
+ // All selectors are potentially relevant until proven otherwise
115
+ return true;
116
+ }
117
+
118
+ /**
119
+ * Get page identifier for debugging/logging
120
+ */
121
+ getPageId(): string {
122
+ const context = this.getCurrentContext();
123
+ return `${context.pathname}${context.hash}${context.search}`;
124
+ }
125
+
126
+ /**
127
+ * Clean up resources
128
+ */
129
+ destroy(): void {
130
+ if (!this.initialized) return;
131
+
132
+ console.debug('[DAP] PageContextService: Destroying...');
133
+
134
+ window.removeEventListener('popstate', this.handlePopState.bind(this));
135
+ this.restoreHistoryMethods();
136
+
137
+ this.handlers.clear();
138
+ this.currentContext = null;
139
+ this.initialized = false;
140
+ }
141
+
142
+ // Private methods
143
+
144
+ private captureCurrentContext(): PageContext {
145
+ return {
146
+ href: window.location.href,
147
+ pathname: window.location.pathname,
148
+ hash: window.location.hash,
149
+ search: window.location.search,
150
+ timestamp: Date.now()
151
+ };
152
+ }
153
+
154
+ private hasContextChanged(previous: PageContext | null, current: PageContext): boolean {
155
+ if (!previous) return true;
156
+
157
+ return (
158
+ previous.href !== current.href ||
159
+ previous.pathname !== current.pathname ||
160
+ previous.hash !== current.hash ||
161
+ previous.search !== current.search
162
+ );
163
+ }
164
+
165
+ private handlePopState(event: PopStateEvent): void {
166
+ console.debug('[DAP] PageContextService: Popstate event detected');
167
+ this.updateContext('navigation');
168
+ }
169
+
170
+ private originalPushState = history.pushState;
171
+ private originalReplaceState = history.replaceState;
172
+
173
+ private interceptHistoryMethods(): void {
174
+ const self = this;
175
+
176
+ // Intercept pushState for SPA navigation
177
+ history.pushState = function(state: any, title: string, url?: string | URL | null) {
178
+ self.originalPushState.apply(history, arguments as any);
179
+ console.debug('[DAP] PageContextService: PushState detected:', url);
180
+
181
+ // Use setTimeout to ensure URL has updated
182
+ setTimeout(() => {
183
+ self.updateContext('navigation');
184
+ }, 0);
185
+ };
186
+
187
+ // Intercept replaceState for SPA navigation
188
+ history.replaceState = function(state: any, title: string, url?: string | URL | null) {
189
+ self.originalReplaceState.apply(history, arguments as any);
190
+ console.debug('[DAP] PageContextService: ReplaceState detected:', url);
191
+
192
+ // Use setTimeout to ensure URL has updated
193
+ setTimeout(() => {
194
+ self.updateContext('navigation');
195
+ }, 0);
196
+ };
197
+ }
198
+
199
+ private restoreHistoryMethods(): void {
200
+ history.pushState = this.originalPushState;
201
+ history.replaceState = this.originalReplaceState;
202
+ }
203
+
204
+ private notifyHandlers(event: PageChangeEvent): void {
205
+ console.debug('[DAP] PageContextService: Notifying handlers:', event.type, {
206
+ from: event.previous?.pathname,
207
+ to: event.current.pathname
208
+ });
209
+
210
+ this.handlers.forEach(handler => {
211
+ try {
212
+ handler(event);
213
+ } catch (error) {
214
+ console.error('[DAP] PageContextService: Handler error:', error);
215
+ }
216
+ });
217
+ }
218
+ }
219
+
220
+ // Export singleton instance
221
+ export const pageContextService = PageContextService.getInstance();