@prosdevlab/experience-sdk-plugins 0.2.0 → 0.3.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.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +120 -0
- package/README.md +141 -79
- package/dist/index.d.ts +206 -35
- package/dist/index.js +1229 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +63 -62
- package/src/exit-intent/exit-intent.ts +2 -3
- package/src/index.ts +2 -0
- package/src/inline/index.ts +3 -0
- package/src/inline/inline.test.ts +620 -0
- package/src/inline/inline.ts +269 -0
- package/src/inline/insertion.ts +66 -0
- package/src/inline/types.ts +52 -0
- package/src/integration.test.ts +356 -297
- package/src/modal/form-rendering.ts +262 -0
- package/src/modal/form-styles.ts +212 -0
- package/src/modal/form-validation.test.ts +413 -0
- package/src/modal/form-validation.ts +126 -0
- package/src/modal/index.ts +3 -0
- package/src/modal/modal-styles.ts +204 -0
- package/src/modal/modal.browser.test.ts +164 -0
- package/src/modal/modal.test.ts +1294 -0
- package/src/modal/modal.ts +685 -0
- package/src/modal/types.ts +114 -0
- package/src/scroll-depth/scroll-depth.test.ts +35 -0
- package/src/scroll-depth/scroll-depth.ts +2 -4
- package/src/time-delay/time-delay.test.ts +2 -2
- package/src/time-delay/time-delay.ts +2 -3
- package/src/types.ts +20 -36
- package/src/utils/sanitize.ts +4 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { PluginFunction } from '@lytics/sdk-kit';
|
|
2
|
+
import type { ExperienceButton } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface ModalConfig {
|
|
5
|
+
modal?: {
|
|
6
|
+
/** Allow dismissal via close button (default: true) */
|
|
7
|
+
dismissable?: boolean;
|
|
8
|
+
/** Allow dismissal via backdrop click (default: true) */
|
|
9
|
+
backdropDismiss?: boolean;
|
|
10
|
+
/** Z-index for modal (default: 10001) */
|
|
11
|
+
zIndex?: number;
|
|
12
|
+
/** Modal size (default: 'md') */
|
|
13
|
+
size?: 'sm' | 'md' | 'lg' | 'fullscreen' | 'auto';
|
|
14
|
+
/** Auto-fullscreen on mobile screens <640px (default: true for 'lg', false for others) */
|
|
15
|
+
mobileFullscreen?: boolean;
|
|
16
|
+
/** Modal position (default: 'center') */
|
|
17
|
+
position?: 'center' | 'bottom';
|
|
18
|
+
/** Animation type (default: 'fade') */
|
|
19
|
+
animation?: 'fade' | 'slide-up' | 'none';
|
|
20
|
+
/** Animation duration in ms (default: 200) */
|
|
21
|
+
animationDuration?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ModalContent {
|
|
26
|
+
/** Optional hero image at top of modal */
|
|
27
|
+
image?: {
|
|
28
|
+
/** Image source URL */
|
|
29
|
+
src: string;
|
|
30
|
+
/** Alt text for accessibility */
|
|
31
|
+
alt: string;
|
|
32
|
+
/** Max height in pixels (default: 300, 200 on mobile) */
|
|
33
|
+
maxHeight?: number;
|
|
34
|
+
};
|
|
35
|
+
/** Modal title */
|
|
36
|
+
title?: string;
|
|
37
|
+
/** Modal message (supports HTML via sanitizer) */
|
|
38
|
+
message: string;
|
|
39
|
+
/** Array of action buttons */
|
|
40
|
+
buttons?: ExperienceButton[];
|
|
41
|
+
/** Optional form configuration */
|
|
42
|
+
form?: FormConfig;
|
|
43
|
+
/** Custom CSS class */
|
|
44
|
+
className?: string;
|
|
45
|
+
/** Inline styles */
|
|
46
|
+
style?: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FormConfig {
|
|
50
|
+
/** Array of form fields */
|
|
51
|
+
fields: FormField[];
|
|
52
|
+
/** Submit button configuration */
|
|
53
|
+
submitButton: ExperienceButton;
|
|
54
|
+
/** Success state after submission */
|
|
55
|
+
successState?: FormState;
|
|
56
|
+
/** Error state on submission failure */
|
|
57
|
+
errorState?: FormState;
|
|
58
|
+
/** Custom validation function (optional) */
|
|
59
|
+
validate?: (data: Record<string, string>) => ValidationResult;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FormField {
|
|
63
|
+
/** Field name (used in form data) */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Field type */
|
|
66
|
+
type: 'email' | 'text' | 'textarea' | 'tel' | 'url' | 'number';
|
|
67
|
+
/** Label text (optional) */
|
|
68
|
+
label?: string;
|
|
69
|
+
/** Placeholder text */
|
|
70
|
+
placeholder?: string;
|
|
71
|
+
/** Required field (default: false) */
|
|
72
|
+
required?: boolean;
|
|
73
|
+
/** Custom validation pattern (regex) */
|
|
74
|
+
pattern?: string;
|
|
75
|
+
/** Error message for validation failure */
|
|
76
|
+
errorMessage?: string;
|
|
77
|
+
/** Custom CSS class */
|
|
78
|
+
className?: string;
|
|
79
|
+
/** Inline styles */
|
|
80
|
+
style?: Record<string, string>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface FormState {
|
|
84
|
+
/** Title to show in success/error state */
|
|
85
|
+
title?: string;
|
|
86
|
+
/** Message to show */
|
|
87
|
+
message: string;
|
|
88
|
+
/** Optional buttons (e.g., "Close", "Try Again") */
|
|
89
|
+
buttons?: ExperienceButton[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ValidationResult {
|
|
93
|
+
/** Whether validation passed */
|
|
94
|
+
valid: boolean;
|
|
95
|
+
/** Validation errors by field name */
|
|
96
|
+
errors?: Record<string, string>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ModalPlugin {
|
|
100
|
+
/** Show a modal experience */
|
|
101
|
+
show(experience: any): void;
|
|
102
|
+
/** Remove a specific modal */
|
|
103
|
+
remove(experienceId: string): void;
|
|
104
|
+
/** Check if a modal is showing */
|
|
105
|
+
isShowing(experienceId?: string): boolean;
|
|
106
|
+
/** Show form success or error state */
|
|
107
|
+
showFormState(experienceId: string, state: 'success' | 'error'): void;
|
|
108
|
+
/** Reset form to initial state */
|
|
109
|
+
resetForm(experienceId: string): void;
|
|
110
|
+
/** Get current form data */
|
|
111
|
+
getFormData(experienceId: string): Record<string, string> | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type { PluginFunction };
|
|
@@ -501,6 +501,41 @@ describe('scrollDepthPlugin', () => {
|
|
|
501
501
|
});
|
|
502
502
|
});
|
|
503
503
|
|
|
504
|
+
describe('reset()', () => {
|
|
505
|
+
it('should clear triggered thresholds and max scroll', async () => {
|
|
506
|
+
const emitSpy = vi.fn();
|
|
507
|
+
|
|
508
|
+
await initPlugin({ thresholds: [25, 50, 75] });
|
|
509
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
510
|
+
vi.advanceTimersByTime(0);
|
|
511
|
+
|
|
512
|
+
// Scroll to 50%
|
|
513
|
+
simulateScroll(1000, 3000, 1000);
|
|
514
|
+
vi.advanceTimersByTime(200);
|
|
515
|
+
|
|
516
|
+
// Should have triggered 25% and 50%
|
|
517
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50]);
|
|
518
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBeGreaterThan(0);
|
|
519
|
+
expect(emitSpy).toHaveBeenCalledTimes(2);
|
|
520
|
+
|
|
521
|
+
// Reset
|
|
522
|
+
sdk.scrollDepth.reset();
|
|
523
|
+
|
|
524
|
+
// Should clear state
|
|
525
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([]);
|
|
526
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(0);
|
|
527
|
+
|
|
528
|
+
// Scroll again to 50% should trigger again
|
|
529
|
+
emitSpy.mockClear();
|
|
530
|
+
simulateScroll(1000, 3000, 1000);
|
|
531
|
+
vi.advanceTimersByTime(200);
|
|
532
|
+
|
|
533
|
+
// Should trigger both 25% and 50% again
|
|
534
|
+
expect(emitSpy).toHaveBeenCalledTimes(2);
|
|
535
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50]);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
504
539
|
describe('Pathfora compatibility tests', () => {
|
|
505
540
|
it('should match Pathfora test: scrollPercentageToDisplay 50', async () => {
|
|
506
541
|
const emitSpy = vi.fn();
|
|
@@ -329,10 +329,9 @@ export const scrollDepthPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
// Setup destroy handler
|
|
332
|
-
|
|
332
|
+
instance.on('sdk:destroy', () => {
|
|
333
333
|
cleanup();
|
|
334
|
-
};
|
|
335
|
-
instance.on('destroy', destroyHandler);
|
|
334
|
+
});
|
|
336
335
|
|
|
337
336
|
// Expose API
|
|
338
337
|
plugin.expose({
|
|
@@ -395,6 +394,5 @@ export const scrollDepthPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
395
394
|
// Return cleanup function
|
|
396
395
|
return () => {
|
|
397
396
|
cleanup();
|
|
398
|
-
instance.off('destroy', destroyHandler);
|
|
399
397
|
};
|
|
400
398
|
};
|
|
@@ -380,7 +380,7 @@ describe('Time Delay Plugin', () => {
|
|
|
380
380
|
vi.advanceTimersByTime(2000);
|
|
381
381
|
|
|
382
382
|
// Destroy SDK
|
|
383
|
-
sdk.
|
|
383
|
+
await sdk.destroy();
|
|
384
384
|
|
|
385
385
|
// Advance past trigger time
|
|
386
386
|
vi.advanceTimersByTime(5000);
|
|
@@ -396,7 +396,7 @@ describe('Time Delay Plugin', () => {
|
|
|
396
396
|
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|
|
397
397
|
|
|
398
398
|
// Destroy SDK
|
|
399
|
-
sdk.
|
|
399
|
+
await sdk.destroy();
|
|
400
400
|
|
|
401
401
|
// Should have removed visibility listener
|
|
402
402
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
|
|
@@ -290,8 +290,7 @@ export const timeDelayPlugin: PluginFunction = (plugin, instance, config) => {
|
|
|
290
290
|
initialize();
|
|
291
291
|
|
|
292
292
|
// Cleanup on instance destroy
|
|
293
|
-
|
|
293
|
+
instance.on('sdk:destroy', () => {
|
|
294
294
|
cleanup();
|
|
295
|
-
};
|
|
296
|
-
instance.on('destroy', destroyHandler);
|
|
295
|
+
});
|
|
297
296
|
};
|
package/src/types.ts
CHANGED
|
@@ -3,10 +3,25 @@
|
|
|
3
3
|
* These types are re-exported by core for user convenience
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { InlineContent as _InlineContent } from './inline/types';
|
|
7
|
+
// Import modal and inline content types from their plugins
|
|
8
|
+
import type { ModalContent as _ModalContent } from './modal/types';
|
|
9
|
+
export type ModalContent = _ModalContent;
|
|
10
|
+
export type InlineContent = _InlineContent;
|
|
11
|
+
|
|
6
12
|
/**
|
|
7
|
-
* Experience
|
|
13
|
+
* Experience button configuration (used across all experience types)
|
|
8
14
|
*/
|
|
9
|
-
export
|
|
15
|
+
export interface ExperienceButton {
|
|
16
|
+
text: string;
|
|
17
|
+
action?: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
variant?: 'primary' | 'secondary' | 'link';
|
|
20
|
+
dismiss?: boolean;
|
|
21
|
+
metadata?: Record<string, any>;
|
|
22
|
+
className?: string;
|
|
23
|
+
style?: Record<string, string>;
|
|
24
|
+
}
|
|
10
25
|
|
|
11
26
|
/**
|
|
12
27
|
* Banner content configuration
|
|
@@ -14,31 +29,13 @@ export type ExperienceContent = BannerContent | ModalContent | TooltipContent;
|
|
|
14
29
|
export interface BannerContent {
|
|
15
30
|
title?: string;
|
|
16
31
|
message: string;
|
|
17
|
-
buttons?:
|
|
18
|
-
text: string;
|
|
19
|
-
action?: string;
|
|
20
|
-
url?: string;
|
|
21
|
-
variant?: 'primary' | 'secondary' | 'link';
|
|
22
|
-
metadata?: Record<string, any>;
|
|
23
|
-
className?: string;
|
|
24
|
-
style?: Record<string, string>;
|
|
25
|
-
}>;
|
|
32
|
+
buttons?: ExperienceButton[];
|
|
26
33
|
dismissable?: boolean;
|
|
27
34
|
position?: 'top' | 'bottom';
|
|
28
35
|
className?: string;
|
|
29
36
|
style?: Record<string, string>;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
/**
|
|
33
|
-
* Modal content configuration
|
|
34
|
-
*/
|
|
35
|
-
export interface ModalContent {
|
|
36
|
-
title: string;
|
|
37
|
-
message: string;
|
|
38
|
-
confirmText?: string;
|
|
39
|
-
cancelText?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
39
|
/**
|
|
43
40
|
* Tooltip content configuration
|
|
44
41
|
*/
|
|
@@ -48,22 +45,9 @@ export interface TooltipContent {
|
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
/**
|
|
51
|
-
*
|
|
52
|
-
*/
|
|
53
|
-
export interface ModalContent {
|
|
54
|
-
title: string;
|
|
55
|
-
message: string;
|
|
56
|
-
confirmText?: string;
|
|
57
|
-
cancelText?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Tooltip content configuration
|
|
48
|
+
* Experience content - varies by type
|
|
62
49
|
*/
|
|
63
|
-
export
|
|
64
|
-
message: string;
|
|
65
|
-
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
66
|
-
}
|
|
50
|
+
export type ExperienceContent = BannerContent | ModalContent | InlineContent | TooltipContent;
|
|
67
51
|
|
|
68
52
|
/**
|
|
69
53
|
* Experience definition
|
package/src/utils/sanitize.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Allowed HTML tags for sanitization
|
|
12
12
|
* Only safe formatting tags are permitted
|
|
13
13
|
*/
|
|
14
|
-
const ALLOWED_TAGS = ['strong', 'em', 'a', 'br', 'span', 'b', 'i', 'p'] as const;
|
|
14
|
+
const ALLOWED_TAGS = ['strong', 'em', 'a', 'br', 'span', 'b', 'i', 'p', 'div', 'ul', 'li'] as const;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Allowed attributes per tag
|
|
@@ -20,6 +20,9 @@ const ALLOWED_ATTRIBUTES: Record<string, string[]> = {
|
|
|
20
20
|
a: ['href', 'class', 'style', 'title'],
|
|
21
21
|
span: ['class', 'style'],
|
|
22
22
|
p: ['class', 'style'],
|
|
23
|
+
div: ['class', 'style'],
|
|
24
|
+
ul: ['class', 'style'],
|
|
25
|
+
li: ['class', 'style'],
|
|
23
26
|
// Other tags have no attributes allowed
|
|
24
27
|
};
|
|
25
28
|
|