@capillarytech/creatives-library 8.0.207 → 8.0.209
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/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/package.json +16 -2
- package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
- package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
- package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
- package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
- package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
- package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
- package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
- package/v2Components/HtmlEditor/constants.js +241 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
- package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
- package/v2Components/HtmlEditor/index.js +29 -0
- package/v2Components/HtmlEditor/index.lazy.js +114 -0
- package/v2Components/HtmlEditor/messages.js +389 -0
- package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
- package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
- package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
- package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
- package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
- package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
- package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
- package/v2Containers/EmailWrapper/index.js +8 -1
- package/v2Containers/Templates/constants.js +8 -0
- package/v2Containers/Templates/index.js +56 -28
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
- package/v2Containers/Whatsapp/constants.js +26 -2
- package/v2Containers/Whatsapp/index.js +4 -1
- package/v2Containers/Whatsapp/tests/index.test.js +460 -18
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEditorContent - Custom hook for managing unified editor content state
|
|
3
|
+
*
|
|
4
|
+
* Handles unified HTML content with embedded CSS and JavaScript
|
|
5
|
+
* Simplified from separate language management to single editor approach
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
9
|
+
import { PERFORMANCE } from '../constants';
|
|
10
|
+
import { useSmartDebounce, analyzeContentSize, useCleanup } from '../utils/simplePerformance';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Custom hook for unified editor content management
|
|
14
|
+
*
|
|
15
|
+
* @param {string} initialContent - Initial unified HTML content
|
|
16
|
+
* @param {Object} options - Configuration options
|
|
17
|
+
* @returns {Object} Content state and methods
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_CONTENT = `<!DOCTYPE html>
|
|
20
|
+
<html lang="en">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="UTF-8">
|
|
23
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
24
|
+
<title>HTML Editor Preview</title>
|
|
25
|
+
<style>
|
|
26
|
+
/* Your CSS styles here */
|
|
27
|
+
body {
|
|
28
|
+
font-family: Arial, sans-serif;
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 20px;
|
|
31
|
+
background-color: #f5f5f5;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.container {
|
|
35
|
+
max-width: 800px;
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
background: white;
|
|
38
|
+
padding: 20px;
|
|
39
|
+
border-radius: 8px;
|
|
40
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<div class="container">
|
|
46
|
+
<h1>Welcome to HTML Editor</h1>
|
|
47
|
+
<p>Start editing your HTML, CSS, and JavaScript code above to see the live preview here.</p>
|
|
48
|
+
<button id="demoButton">Click me!</button>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<script>
|
|
52
|
+
// Your JavaScript code here
|
|
53
|
+
document.getElementById('demoButton').addEventListener('click', function() {
|
|
54
|
+
alert('Hello from the HTML Editor!');
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
</body>
|
|
58
|
+
</html>`;
|
|
59
|
+
|
|
60
|
+
export const useEditorContent = (
|
|
61
|
+
initialContent = null,
|
|
62
|
+
options = {}
|
|
63
|
+
) => {
|
|
64
|
+
const {
|
|
65
|
+
autoSave = true,
|
|
66
|
+
autoSaveInterval = PERFORMANCE.AUTO_SAVE_INTERVAL,
|
|
67
|
+
onSave = null,
|
|
68
|
+
onChange = null
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
// Unified content state
|
|
72
|
+
const [content, setContent] = useState(initialContent || DEFAULT_CONTENT);
|
|
73
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
74
|
+
const [lastSaved, setLastSaved] = useState(null);
|
|
75
|
+
const [isAutoSaveEnabled, setIsAutoSaveEnabled] = useState(autoSave);
|
|
76
|
+
|
|
77
|
+
// Refs for performance optimization
|
|
78
|
+
const onChangeDebounceRef = useRef(null);
|
|
79
|
+
const validationDebounceRef = useRef(null);
|
|
80
|
+
const autoSaveRef = useRef(null);
|
|
81
|
+
const validationCallbackRef = useRef(null);
|
|
82
|
+
const lastContentRef = useRef(initialContent || DEFAULT_CONTENT);
|
|
83
|
+
|
|
84
|
+
// Smart debouncing hooks
|
|
85
|
+
const debouncedOnChange = useSmartDebounce(onChange || (() => {}), PERFORMANCE.PREVIEW_UPDATE_DEBOUNCE);
|
|
86
|
+
const debouncedValidation = useSmartDebounce((content) => {
|
|
87
|
+
if (validationCallbackRef.current) {
|
|
88
|
+
validationCallbackRef.current(content);
|
|
89
|
+
}
|
|
90
|
+
}, PERFORMANCE.VALIDATION_DEBOUNCE);
|
|
91
|
+
|
|
92
|
+
// Update unified content
|
|
93
|
+
const updateContent = useCallback((value, immediate = false) => {
|
|
94
|
+
setContent(prevContent => {
|
|
95
|
+
// Skip update if content hasn't actually changed
|
|
96
|
+
if (prevContent === value) {
|
|
97
|
+
return prevContent;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Analyze content for performance optimization
|
|
101
|
+
const analysis = analyzeContentSize(value);
|
|
102
|
+
|
|
103
|
+
// Use smart debouncing for callbacks
|
|
104
|
+
if (onChange) {
|
|
105
|
+
if (immediate) {
|
|
106
|
+
onChange(value);
|
|
107
|
+
} else {
|
|
108
|
+
debouncedOnChange(value, analysis.size);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Use smart debouncing for validation
|
|
113
|
+
if (validationCallbackRef.current) {
|
|
114
|
+
if (immediate) {
|
|
115
|
+
validationCallbackRef.current(value);
|
|
116
|
+
} else {
|
|
117
|
+
debouncedValidation(value, analysis.size);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Update refs
|
|
122
|
+
lastContentRef.current = value;
|
|
123
|
+
|
|
124
|
+
return value;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
setIsDirty(true);
|
|
128
|
+
}, [onChange, debouncedOnChange, debouncedValidation]);
|
|
129
|
+
|
|
130
|
+
// Get current unified content
|
|
131
|
+
const currentContent = useMemo(() => {
|
|
132
|
+
return content || '';
|
|
133
|
+
}, [content]);
|
|
134
|
+
|
|
135
|
+
// Check if content exists
|
|
136
|
+
const hasContent = useCallback(() => {
|
|
137
|
+
return content && typeof content === 'string' && content.trim().length > 0;
|
|
138
|
+
}, [content]);
|
|
139
|
+
|
|
140
|
+
// Get content size for performance monitoring
|
|
141
|
+
const getContentSize = useCallback(() => {
|
|
142
|
+
return (content && typeof content === 'string') ? content.length : 0;
|
|
143
|
+
}, [content]);
|
|
144
|
+
|
|
145
|
+
// Performance analysis
|
|
146
|
+
const contentAnalysis = useMemo(() => {
|
|
147
|
+
return analyzeContentSize(content);
|
|
148
|
+
}, [content]);
|
|
149
|
+
|
|
150
|
+
// Check if content is large (for performance warnings)
|
|
151
|
+
const isLargeContent = useMemo(() => {
|
|
152
|
+
return contentAnalysis.isLarge;
|
|
153
|
+
}, [contentAnalysis.isLarge]);
|
|
154
|
+
|
|
155
|
+
// Mark content as saved
|
|
156
|
+
const markAsSaved = useCallback(() => {
|
|
157
|
+
setIsDirty(false);
|
|
158
|
+
setLastSaved(new Date());
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
// Reset all content
|
|
162
|
+
const resetContent = useCallback(() => {
|
|
163
|
+
const contentToUse = initialContent || DEFAULT_CONTENT;
|
|
164
|
+
setContent(contentToUse);
|
|
165
|
+
setIsDirty(false);
|
|
166
|
+
setLastSaved(null);
|
|
167
|
+
lastContentRef.current = contentToUse;
|
|
168
|
+
}, [initialContent]);
|
|
169
|
+
|
|
170
|
+
// Save content (calls onSave callback if provided)
|
|
171
|
+
const saveContent = useCallback(async () => {
|
|
172
|
+
if (onSave && isDirty) {
|
|
173
|
+
try {
|
|
174
|
+
await onSave(content);
|
|
175
|
+
markAsSaved();
|
|
176
|
+
return true;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('Failed to save content:', error);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}, [content, isDirty, onSave, markAsSaved]);
|
|
184
|
+
|
|
185
|
+
// Register validation callback
|
|
186
|
+
const registerValidationCallback = useCallback((callback) => {
|
|
187
|
+
validationCallbackRef.current = callback;
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
// Unregister validation callback
|
|
191
|
+
const unregisterValidationCallback = useCallback(() => {
|
|
192
|
+
validationCallbackRef.current = null;
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
// Toggle auto-save
|
|
196
|
+
const toggleAutoSave = useCallback(() => {
|
|
197
|
+
setIsAutoSaveEnabled(prev => !prev);
|
|
198
|
+
}, []);
|
|
199
|
+
|
|
200
|
+
// Force validation
|
|
201
|
+
const validateContent = useCallback(() => {
|
|
202
|
+
if (validationCallbackRef.current) {
|
|
203
|
+
validationCallbackRef.current(content);
|
|
204
|
+
}
|
|
205
|
+
}, [content]);
|
|
206
|
+
|
|
207
|
+
// Auto-save effect
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (isAutoSaveEnabled && isDirty && autoSaveInterval > 0) {
|
|
210
|
+
autoSaveRef.current = setTimeout(() => {
|
|
211
|
+
saveContent();
|
|
212
|
+
}, autoSaveInterval);
|
|
213
|
+
|
|
214
|
+
return () => {
|
|
215
|
+
if (autoSaveRef.current) {
|
|
216
|
+
clearTimeout(autoSaveRef.current);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}, [isDirty, isAutoSaveEnabled, autoSaveInterval, saveContent]);
|
|
221
|
+
|
|
222
|
+
// Cleanup effect with performance optimization
|
|
223
|
+
useCleanup(() => {
|
|
224
|
+
if (onChangeDebounceRef.current) {
|
|
225
|
+
clearTimeout(onChangeDebounceRef.current);
|
|
226
|
+
onChangeDebounceRef.current = null;
|
|
227
|
+
}
|
|
228
|
+
if (validationDebounceRef.current) {
|
|
229
|
+
clearTimeout(validationDebounceRef.current);
|
|
230
|
+
validationDebounceRef.current = null;
|
|
231
|
+
}
|
|
232
|
+
if (autoSaveRef.current) {
|
|
233
|
+
clearTimeout(autoSaveRef.current);
|
|
234
|
+
autoSaveRef.current = null;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Return state and methods
|
|
239
|
+
return {
|
|
240
|
+
// Content state
|
|
241
|
+
content: currentContent,
|
|
242
|
+
isDirty,
|
|
243
|
+
lastSaved,
|
|
244
|
+
isAutoSaveEnabled,
|
|
245
|
+
isLargeContent,
|
|
246
|
+
|
|
247
|
+
// Content methods
|
|
248
|
+
updateContent,
|
|
249
|
+
hasContent,
|
|
250
|
+
getContentSize,
|
|
251
|
+
resetContent,
|
|
252
|
+
saveContent,
|
|
253
|
+
markAsSaved,
|
|
254
|
+
|
|
255
|
+
// Auto-save methods
|
|
256
|
+
toggleAutoSave,
|
|
257
|
+
|
|
258
|
+
// Validation methods
|
|
259
|
+
registerValidationCallback,
|
|
260
|
+
unregisterValidationCallback,
|
|
261
|
+
validateContent,
|
|
262
|
+
|
|
263
|
+
// Computed properties
|
|
264
|
+
canSave: isDirty && onSave !== null,
|
|
265
|
+
isEmpty: !hasContent(),
|
|
266
|
+
characterCount: getContentSize(),
|
|
267
|
+
|
|
268
|
+
// Performance indicators
|
|
269
|
+
shouldShowPerformanceWarning: isLargeContent,
|
|
270
|
+
contentAnalysis
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export default useEditorContent;
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInAppContent - Custom hook for managing device-specific content in InApp variant
|
|
3
|
+
*
|
|
4
|
+
* Manages separate HTML content for Android and iOS devices with sync functionality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
8
|
+
import { DEVICE_TYPES, PERFORMANCE } from '../constants';
|
|
9
|
+
|
|
10
|
+
// Constants for better maintainability
|
|
11
|
+
const CONTENT_VALIDATION = {
|
|
12
|
+
MIN_CONTENT_LENGTH: 0,
|
|
13
|
+
DEFAULT_CONTENT_TYPE: 'string'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const AUTO_SAVE_CONFIG = {
|
|
17
|
+
DEFAULT_ENABLED: true,
|
|
18
|
+
DEFAULT_INTERVAL: PERFORMANCE.AUTO_SAVE_INTERVAL,
|
|
19
|
+
MIN_AUTO_SAVE_INTERVAL_MS: 1000 // Minimum 1 second between auto-saves
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default InApp content for different devices
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_INAPP_CONTENT = {
|
|
26
|
+
[DEVICE_TYPES.ANDROID]: `<!DOCTYPE html>
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<head>
|
|
29
|
+
<meta charset="UTF-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
31
|
+
<title>In-App Notification</title>
|
|
32
|
+
<style>
|
|
33
|
+
body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: 16px;
|
|
36
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;
|
|
37
|
+
background-color: #ffffff;
|
|
38
|
+
color: #212121;
|
|
39
|
+
}
|
|
40
|
+
.notification {
|
|
41
|
+
max-width: 100%;
|
|
42
|
+
background: white;
|
|
43
|
+
border-radius: 8px;
|
|
44
|
+
padding: 16px;
|
|
45
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
46
|
+
}
|
|
47
|
+
.title {
|
|
48
|
+
font-size: 16px;
|
|
49
|
+
font-weight: 500;
|
|
50
|
+
margin: 0 0 8px 0;
|
|
51
|
+
color: #212121;
|
|
52
|
+
}
|
|
53
|
+
.message {
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
line-height: 1.4;
|
|
56
|
+
margin: 0 0 16px 0;
|
|
57
|
+
color: #424242;
|
|
58
|
+
}
|
|
59
|
+
.cta-button {
|
|
60
|
+
background-color: #42b040;
|
|
61
|
+
color: white;
|
|
62
|
+
border: none;
|
|
63
|
+
border-radius: 4px;
|
|
64
|
+
padding: 8px 16px;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
font-weight: 500;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
width: 100%;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<div class="notification">
|
|
74
|
+
<h2 class="title">Sample template</h2>
|
|
75
|
+
<p class="message">This is a sample template for in-app notification content. This can be triggered on any behavioural event while the user is on the app</p>
|
|
76
|
+
<button class="cta-button">Add to cart</button>
|
|
77
|
+
</div>
|
|
78
|
+
</body>
|
|
79
|
+
</html>`,
|
|
80
|
+
[DEVICE_TYPES.IOS]: `<!DOCTYPE html>
|
|
81
|
+
<html lang="en">
|
|
82
|
+
<head>
|
|
83
|
+
<meta charset="UTF-8">
|
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
85
|
+
<title>In-App Notification</title>
|
|
86
|
+
<style>
|
|
87
|
+
body {
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 16px;
|
|
90
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
|
|
91
|
+
background-color: #ffffff;
|
|
92
|
+
color: #000000;
|
|
93
|
+
}
|
|
94
|
+
.notification {
|
|
95
|
+
max-width: 100%;
|
|
96
|
+
background: white;
|
|
97
|
+
border-radius: 12px;
|
|
98
|
+
padding: 16px;
|
|
99
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
|
100
|
+
}
|
|
101
|
+
.title {
|
|
102
|
+
font-size: 17px;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
margin: 0 0 8px 0;
|
|
105
|
+
color: #000000;
|
|
106
|
+
}
|
|
107
|
+
.message {
|
|
108
|
+
font-size: 15px;
|
|
109
|
+
line-height: 1.4;
|
|
110
|
+
margin: 0 0 16px 0;
|
|
111
|
+
color: #3c3c43;
|
|
112
|
+
}
|
|
113
|
+
.cta-button {
|
|
114
|
+
background-color: #007AFF;
|
|
115
|
+
color: white;
|
|
116
|
+
border: none;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
padding: 12px 16px;
|
|
119
|
+
font-size: 16px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
width: 100%;
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<div class="notification">
|
|
128
|
+
<h2 class="title">Sample template</h2>
|
|
129
|
+
<p class="message">This is a sample template for in-app notification content. This can be triggered on any behavioural event while the user is on the app</p>
|
|
130
|
+
<button class="cta-button">Add to cart</button>
|
|
131
|
+
</div>
|
|
132
|
+
</body>
|
|
133
|
+
</html>`
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Custom hook for InApp content management with device-specific content
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} initialContent - Initial content for both devices
|
|
140
|
+
* @param {Object} options - Configuration options
|
|
141
|
+
* @param {boolean} options.autoSave - Enable auto-save functionality
|
|
142
|
+
* @param {number} options.autoSaveInterval - Auto-save interval in milliseconds
|
|
143
|
+
* @param {Function} options.onSave - Callback when content is saved
|
|
144
|
+
* @param {Function} options.onChange - Callback when content changes
|
|
145
|
+
* @returns {Object} Content state and methods
|
|
146
|
+
*/
|
|
147
|
+
export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
148
|
+
// Destructure options with better defaults
|
|
149
|
+
const {
|
|
150
|
+
autoSave = AUTO_SAVE_CONFIG.DEFAULT_ENABLED,
|
|
151
|
+
autoSaveInterval = AUTO_SAVE_CONFIG.DEFAULT_INTERVAL,
|
|
152
|
+
onSave,
|
|
153
|
+
onChange
|
|
154
|
+
} = options;
|
|
155
|
+
|
|
156
|
+
// Destructure device types for cleaner code
|
|
157
|
+
const { ANDROID, IOS } = DEVICE_TYPES;
|
|
158
|
+
|
|
159
|
+
// Device-specific content state with optional chaining
|
|
160
|
+
const [deviceContent, setDeviceContent] = useState(() => ({
|
|
161
|
+
[ANDROID]: initialContent?.[ANDROID] || DEFAULT_INAPP_CONTENT[ANDROID],
|
|
162
|
+
[IOS]: initialContent?.[IOS] || DEFAULT_INAPP_CONTENT[IOS]
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
// Current active device
|
|
166
|
+
const [activeDevice, setActiveDevice] = useState(ANDROID);
|
|
167
|
+
|
|
168
|
+
// Content sync state
|
|
169
|
+
const [keepContentSame, setKeepContentSame] = useState(false);
|
|
170
|
+
|
|
171
|
+
// Dirty state tracking
|
|
172
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
173
|
+
const [lastSaved, setLastSaved] = useState(null);
|
|
174
|
+
|
|
175
|
+
// Auto-save timer
|
|
176
|
+
const autoSaveTimerRef = useRef(null);
|
|
177
|
+
const changeTimestampRef = useRef(null);
|
|
178
|
+
|
|
179
|
+
// Refs to store current values for auto-save
|
|
180
|
+
const onSaveRef = useRef(onSave);
|
|
181
|
+
const deviceContentRef = useRef(deviceContent);
|
|
182
|
+
|
|
183
|
+
// Update refs when values change
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
onSaveRef.current = onSave;
|
|
186
|
+
}, [onSave]);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
deviceContentRef.current = deviceContent;
|
|
190
|
+
}, [deviceContent]);
|
|
191
|
+
|
|
192
|
+
// Get current active content
|
|
193
|
+
const currentContent = useMemo(() => {
|
|
194
|
+
return deviceContent[activeDevice] || '';
|
|
195
|
+
}, [deviceContent, activeDevice]);
|
|
196
|
+
|
|
197
|
+
// Update content for current device
|
|
198
|
+
const updateContent = useCallback((newContent) => {
|
|
199
|
+
// Validate input
|
|
200
|
+
if (typeof newContent !== CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE) {
|
|
201
|
+
console.warn('useInAppContent: newContent must be a string');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Create the updated content object
|
|
206
|
+
let updatedDeviceContent;
|
|
207
|
+
|
|
208
|
+
if (keepContentSame) {
|
|
209
|
+
// When sync is enabled, update both devices with the same content
|
|
210
|
+
updatedDeviceContent = {
|
|
211
|
+
[ANDROID]: newContent,
|
|
212
|
+
[IOS]: newContent
|
|
213
|
+
};
|
|
214
|
+
} else {
|
|
215
|
+
// When sync is disabled, update only the current device
|
|
216
|
+
setDeviceContent(prev => {
|
|
217
|
+
updatedDeviceContent = {
|
|
218
|
+
...prev,
|
|
219
|
+
[activeDevice]: newContent
|
|
220
|
+
};
|
|
221
|
+
return updatedDeviceContent;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Early return for independent mode to avoid double state update
|
|
225
|
+
setIsDirty(true);
|
|
226
|
+
changeTimestampRef.current = Date.now();
|
|
227
|
+
|
|
228
|
+
// Trigger onChange callback with optional chaining
|
|
229
|
+
onChange?.(updatedDeviceContent, activeDevice);
|
|
230
|
+
|
|
231
|
+
// Setup auto-save for independent mode
|
|
232
|
+
if (autoSave && autoSaveInterval > AUTO_SAVE_CONFIG.MIN_AUTO_SAVE_INTERVAL_MS && newContent.length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH) {
|
|
233
|
+
if (autoSaveTimerRef.current) {
|
|
234
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
235
|
+
}
|
|
236
|
+
autoSaveTimerRef.current = setTimeout(() => {
|
|
237
|
+
// Use current values at time of execution
|
|
238
|
+
setIsDirty(false);
|
|
239
|
+
setLastSaved(new Date());
|
|
240
|
+
|
|
241
|
+
if (autoSaveTimerRef.current) {
|
|
242
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
243
|
+
autoSaveTimerRef.current = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Get the current onSave callback and deviceContent from refs
|
|
247
|
+
const currentOnSave = onSaveRef.current;
|
|
248
|
+
currentOnSave?.(deviceContentRef.current);
|
|
249
|
+
}, autoSaveInterval);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Handle sync mode state update
|
|
256
|
+
setDeviceContent(updatedDeviceContent);
|
|
257
|
+
setIsDirty(true);
|
|
258
|
+
changeTimestampRef.current = Date.now();
|
|
259
|
+
|
|
260
|
+
// Trigger onChange callback with optional chaining
|
|
261
|
+
onChange?.(updatedDeviceContent, activeDevice);
|
|
262
|
+
|
|
263
|
+
// Setup auto-save
|
|
264
|
+
if (autoSave && autoSaveInterval > AUTO_SAVE_CONFIG.MIN_AUTO_SAVE_INTERVAL_MS && newContent.length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH) {
|
|
265
|
+
if (autoSaveTimerRef.current) {
|
|
266
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
267
|
+
}
|
|
268
|
+
autoSaveTimerRef.current = setTimeout(() => {
|
|
269
|
+
// Use current values at time of execution
|
|
270
|
+
setIsDirty(false);
|
|
271
|
+
setLastSaved(new Date());
|
|
272
|
+
|
|
273
|
+
if (autoSaveTimerRef.current) {
|
|
274
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
275
|
+
autoSaveTimerRef.current = null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Get the current onSave callback and deviceContent from refs
|
|
279
|
+
const currentOnSave = onSaveRef.current;
|
|
280
|
+
currentOnSave?.(deviceContentRef.current);
|
|
281
|
+
}, autoSaveInterval);
|
|
282
|
+
}
|
|
283
|
+
}, [activeDevice, keepContentSame, autoSave, autoSaveInterval, onChange]);
|
|
284
|
+
|
|
285
|
+
// Switch active device with better validation
|
|
286
|
+
const switchDevice = useCallback((device) => {
|
|
287
|
+
const validDevices = [ANDROID, IOS];
|
|
288
|
+
if (device !== activeDevice && validDevices.includes(device)) {
|
|
289
|
+
setActiveDevice(device);
|
|
290
|
+
} else if (!validDevices.includes(device)) {
|
|
291
|
+
console.warn(`useInAppContent: Invalid device type "${device}". Valid types are: ${validDevices.join(', ')}`);
|
|
292
|
+
}
|
|
293
|
+
}, [activeDevice, ANDROID, IOS]);
|
|
294
|
+
|
|
295
|
+
// Toggle content sync
|
|
296
|
+
const toggleContentSync = useCallback((enabled) => {
|
|
297
|
+
setKeepContentSame(enabled);
|
|
298
|
+
|
|
299
|
+
if (enabled) {
|
|
300
|
+
// When enabling sync, copy current device content to the other device
|
|
301
|
+
const currentActiveContent = deviceContent[activeDevice];
|
|
302
|
+
const syncedContent = {
|
|
303
|
+
[ANDROID]: currentActiveContent,
|
|
304
|
+
[IOS]: currentActiveContent
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
setDeviceContent(syncedContent);
|
|
308
|
+
setIsDirty(true);
|
|
309
|
+
}
|
|
310
|
+
// When disabled (enabled = false), we don't need to do anything special
|
|
311
|
+
// Each device will maintain its current content independently
|
|
312
|
+
}, [activeDevice, deviceContent]);
|
|
313
|
+
|
|
314
|
+
// Mark content as saved
|
|
315
|
+
const markAsSaved = useCallback(() => {
|
|
316
|
+
setIsDirty(false);
|
|
317
|
+
setLastSaved(new Date());
|
|
318
|
+
|
|
319
|
+
if (autoSaveTimerRef.current) {
|
|
320
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
321
|
+
autoSaveTimerRef.current = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
onSave?.(deviceContent);
|
|
325
|
+
}, [deviceContent, onSave]);
|
|
326
|
+
|
|
327
|
+
// Check if content exists for current device
|
|
328
|
+
const hasContent = useMemo(() => {
|
|
329
|
+
return typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE &&
|
|
330
|
+
currentContent.trim().length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH;
|
|
331
|
+
}, [currentContent]);
|
|
332
|
+
|
|
333
|
+
// Get content size for current device
|
|
334
|
+
const getContentSize = useCallback(() => {
|
|
335
|
+
return typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE
|
|
336
|
+
? currentContent.length
|
|
337
|
+
: CONTENT_VALIDATION.MIN_CONTENT_LENGTH;
|
|
338
|
+
}, [currentContent]);
|
|
339
|
+
|
|
340
|
+
// Get content for specific device
|
|
341
|
+
const getDeviceContent = useCallback((device) => {
|
|
342
|
+
return deviceContent[device] || '';
|
|
343
|
+
}, [deviceContent]);
|
|
344
|
+
|
|
345
|
+
// Set content for specific device
|
|
346
|
+
const setDeviceContent_ = useCallback((device, content) => {
|
|
347
|
+
const validDevices = [ANDROID, IOS];
|
|
348
|
+
|
|
349
|
+
if (!validDevices.includes(device)) {
|
|
350
|
+
console.warn(`useInAppContent: Invalid device type "${device}". Valid types are: ${validDevices.join(', ')}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (typeof content !== CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE) {
|
|
355
|
+
console.warn('useInAppContent: content must be a string');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (keepContentSame) {
|
|
360
|
+
// Update both devices when sync is enabled
|
|
361
|
+
setDeviceContent({
|
|
362
|
+
[ANDROID]: content,
|
|
363
|
+
[IOS]: content
|
|
364
|
+
});
|
|
365
|
+
} else {
|
|
366
|
+
// Update specific device
|
|
367
|
+
setDeviceContent(prev => ({
|
|
368
|
+
...prev,
|
|
369
|
+
[device]: content
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
setIsDirty(true);
|
|
373
|
+
}, [keepContentSame, ANDROID, IOS]);
|
|
374
|
+
|
|
375
|
+
// Cleanup on unmount
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
return () => {
|
|
378
|
+
if (autoSaveTimerRef.current) {
|
|
379
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}, []);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
// Current state
|
|
386
|
+
content: currentContent,
|
|
387
|
+
deviceContent,
|
|
388
|
+
activeDevice,
|
|
389
|
+
keepContentSame,
|
|
390
|
+
isDirty,
|
|
391
|
+
lastSaved,
|
|
392
|
+
hasContent,
|
|
393
|
+
|
|
394
|
+
// Content management
|
|
395
|
+
updateContent,
|
|
396
|
+
getDeviceContent,
|
|
397
|
+
setDeviceContent: setDeviceContent_,
|
|
398
|
+
getContentSize,
|
|
399
|
+
|
|
400
|
+
// Device management
|
|
401
|
+
switchDevice,
|
|
402
|
+
toggleContentSync,
|
|
403
|
+
|
|
404
|
+
// Save management
|
|
405
|
+
markAsSaved
|
|
406
|
+
};
|
|
407
|
+
};
|