@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.
Files changed (77) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/package.json +16 -2
  4. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  6. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  8. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  9. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  10. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  11. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  12. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  14. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  15. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  19. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  20. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  21. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  29. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  30. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  31. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  32. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  35. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  36. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  38. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  39. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  42. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  43. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  44. package/v2Components/HtmlEditor/constants.js +241 -0
  45. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  46. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  52. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  53. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  54. package/v2Components/HtmlEditor/index.js +29 -0
  55. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  56. package/v2Components/HtmlEditor/messages.js +389 -0
  57. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  58. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  63. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  64. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  65. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  66. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  67. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  68. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  69. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  70. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  71. package/v2Containers/EmailWrapper/index.js +8 -1
  72. package/v2Containers/Templates/constants.js +8 -0
  73. package/v2Containers/Templates/index.js +56 -28
  74. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
  75. package/v2Containers/Whatsapp/constants.js +26 -2
  76. package/v2Containers/Whatsapp/index.js +4 -1
  77. 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
+ };