@capillarytech/creatives-library 8.0.246-alpha.0 → 8.0.246

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 (133) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/constants/unified.js +1 -2
  4. package/initialReducer.js +0 -2
  5. package/package.json +1 -1
  6. package/services/api.js +0 -10
  7. package/services/tests/api.test.js +0 -18
  8. package/utils/common.js +0 -5
  9. package/utils/commonUtils.js +5 -28
  10. package/utils/tests/commonUtil.test.js +0 -224
  11. package/utils/transformTemplateConfig.js +10 -0
  12. package/v2Components/CapDeviceContent/index.js +56 -61
  13. package/v2Components/CapTagList/index.js +1 -6
  14. package/v2Components/CapTagListWithInput/index.js +1 -5
  15. package/v2Components/CapTagListWithInput/messages.js +1 -1
  16. package/v2Components/CapWhatsappCTA/tests/index.test.js +0 -5
  17. package/v2Components/ErrorInfoNote/index.js +72 -447
  18. package/v2Components/ErrorInfoNote/messages.js +0 -22
  19. package/v2Components/ErrorInfoNote/style.scss +4 -280
  20. package/v2Components/FormBuilder/tests/index.test.js +4 -13
  21. package/v2Components/HtmlEditor/HTMLEditor.js +94 -642
  22. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +133 -1135
  23. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +16 -27
  24. package/v2Components/HtmlEditor/_htmlEditor.scss +45 -108
  25. package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
  26. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +101 -13
  27. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +139 -148
  28. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +1 -2
  29. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  30. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +0 -9
  31. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +1 -1
  32. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +0 -22
  33. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +7 -4
  34. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +45 -35
  35. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +3 -1
  36. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  37. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +6 -7
  38. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +6 -3
  39. package/v2Components/HtmlEditor/components/PreviewPane/index.js +13 -11
  40. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  41. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  42. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +31 -49
  43. package/v2Components/HtmlEditor/constants.js +20 -29
  44. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +16 -373
  45. package/v2Components/HtmlEditor/hooks/useEditorContent.js +2 -5
  46. package/v2Components/HtmlEditor/hooks/useInAppContent.js +146 -88
  47. package/v2Components/HtmlEditor/hooks/useValidation.js +45 -150
  48. package/v2Components/HtmlEditor/index.js +1 -1
  49. package/v2Components/HtmlEditor/messages.js +85 -95
  50. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +102 -134
  51. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +25 -23
  52. package/v2Components/HtmlEditor/utils/validationAdapter.js +41 -66
  53. package/v2Components/MobilePushPreviewV2/index.js +7 -32
  54. package/v2Components/TemplatePreview/_templatePreview.scss +24 -44
  55. package/v2Components/TemplatePreview/index.js +32 -47
  56. package/v2Components/TemplatePreview/messages.js +0 -4
  57. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +0 -1
  58. package/v2Components/TestAndPreviewSlidebox/index.js +25 -31
  59. package/v2Containers/BeeEditor/index.js +90 -172
  60. package/v2Containers/CreativesContainer/SlideBoxContent.js +51 -128
  61. package/v2Containers/CreativesContainer/SlideBoxFooter.js +12 -113
  62. package/v2Containers/CreativesContainer/SlideBoxHeader.js +1 -2
  63. package/v2Containers/CreativesContainer/constants.js +0 -1
  64. package/v2Containers/CreativesContainer/index.js +46 -238
  65. package/v2Containers/CreativesContainer/messages.js +0 -8
  66. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +2 -11
  67. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +50 -38
  68. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +0 -91
  69. package/v2Containers/Email/actions.js +0 -7
  70. package/v2Containers/Email/constants.js +1 -5
  71. package/v2Containers/Email/index.js +30 -229
  72. package/v2Containers/Email/messages.js +0 -32
  73. package/v2Containers/Email/reducer.js +1 -12
  74. package/v2Containers/Email/sagas.js +7 -61
  75. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +0 -2
  76. package/v2Containers/Email/tests/sagas.test.js +1 -1
  77. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +15 -210
  78. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +74 -40
  79. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +67 -2
  80. package/v2Containers/EmailWrapper/constants.js +0 -2
  81. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +77 -629
  82. package/v2Containers/EmailWrapper/index.js +23 -103
  83. package/v2Containers/EmailWrapper/messages.js +1 -61
  84. package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +214 -0
  85. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +77 -509
  86. package/v2Containers/InApp/actions.js +0 -7
  87. package/v2Containers/InApp/constants.js +4 -20
  88. package/v2Containers/InApp/index.js +357 -801
  89. package/v2Containers/InApp/index.scss +3 -4
  90. package/v2Containers/InApp/messages.js +3 -7
  91. package/v2Containers/InApp/reducer.js +3 -21
  92. package/v2Containers/InApp/sagas.js +9 -29
  93. package/v2Containers/InApp/selectors.js +5 -25
  94. package/v2Containers/InApp/tests/index.test.js +50 -154
  95. package/v2Containers/InApp/tests/reducer.test.js +0 -34
  96. package/v2Containers/InApp/tests/sagas.test.js +9 -61
  97. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +0 -3
  98. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +0 -2
  99. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +0 -2
  100. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +0 -9
  101. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +0 -12
  102. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +0 -4
  103. package/v2Containers/TagList/index.js +19 -62
  104. package/v2Containers/Templates/_templates.scss +1 -60
  105. package/v2Containers/Templates/index.js +4 -89
  106. package/v2Containers/Templates/messages.js +0 -4
  107. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +0 -35
  108. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -874
  109. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +0 -254
  110. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +0 -363
  111. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +0 -51
  112. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.apiErrors.test.js +0 -630
  113. package/v2Containers/BeePopupEditor/constants.js +0 -10
  114. package/v2Containers/BeePopupEditor/index.js +0 -193
  115. package/v2Containers/BeePopupEditor/tests/index.test.js +0 -627
  116. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +0 -1317
  117. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +0 -1605
  118. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +0 -520
  119. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +0 -643
  120. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +0 -376
  121. package/v2Containers/InApp/__tests__/sagas.test.js +0 -363
  122. package/v2Containers/InApp/tests/selectors.test.js +0 -612
  123. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +0 -162
  124. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +0 -267
  125. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +0 -9
  126. package/v2Containers/InAppWrapper/constants.js +0 -16
  127. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +0 -473
  128. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +0 -198
  129. package/v2Containers/InAppWrapper/index.js +0 -148
  130. package/v2Containers/InAppWrapper/messages.js +0 -49
  131. package/v2Containers/InappAdvance/index.js +0 -1099
  132. package/v2Containers/InappAdvance/index.scss +0 -10
  133. package/v2Containers/InappAdvance/tests/index.test.js +0 -448
@@ -4,30 +4,133 @@
4
4
  * Manages separate HTML content for Android and iOS devices with sync functionality
5
5
  */
6
6
 
7
- import {
8
- useState, useCallback, useRef, useEffect, useMemo,
9
- } from 'react';
7
+ import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
10
8
  import { DEVICE_TYPES, PERFORMANCE } from '../constants';
11
9
 
12
10
  // Constants for better maintainability
13
11
  const CONTENT_VALIDATION = {
14
12
  MIN_CONTENT_LENGTH: 0,
15
- DEFAULT_CONTENT_TYPE: 'string',
13
+ DEFAULT_CONTENT_TYPE: 'string'
16
14
  };
17
15
 
18
16
  const AUTO_SAVE_CONFIG = {
19
17
  DEFAULT_ENABLED: true,
20
18
  DEFAULT_INTERVAL: PERFORMANCE.AUTO_SAVE_INTERVAL,
21
- MIN_AUTO_SAVE_INTERVAL_MS: 1000, // Minimum 1 second between auto-saves
19
+ MIN_AUTO_SAVE_INTERVAL_MS: 1000 // Minimum 1 second between auto-saves
22
20
  };
23
21
 
24
22
  /**
25
23
  * Default InApp content for different devices
26
- * Empty strings - no default content for new templates
27
24
  */
28
25
  const DEFAULT_INAPP_CONTENT = {
29
- [DEVICE_TYPES.ANDROID]: '',
30
- [DEVICE_TYPES.IOS]: '',
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>`
31
134
  };
32
135
 
33
136
  /**
@@ -47,7 +150,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
47
150
  autoSave = AUTO_SAVE_CONFIG.DEFAULT_ENABLED,
48
151
  autoSaveInterval = AUTO_SAVE_CONFIG.DEFAULT_INTERVAL,
49
152
  onSave,
50
- onChange,
153
+ onChange
51
154
  } = options;
52
155
 
53
156
  // Destructure device types for cleaner code
@@ -56,7 +159,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
56
159
  // Device-specific content state with optional chaining
57
160
  const [deviceContent, setDeviceContent] = useState(() => ({
58
161
  [ANDROID]: initialContent?.[ANDROID] || DEFAULT_INAPP_CONTENT[ANDROID],
59
- [IOS]: initialContent?.[IOS] || DEFAULT_INAPP_CONTENT[IOS],
162
+ [IOS]: initialContent?.[IOS] || DEFAULT_INAPP_CONTENT[IOS]
60
163
  }));
61
164
 
62
165
  // Current active device
@@ -86,67 +189,15 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
86
189
  deviceContentRef.current = deviceContent;
87
190
  }, [deviceContent]);
88
191
 
89
- // Ref to track if we've loaded initial content to prevent overriding user edits
90
- const initialContentLoadedRef = useRef(false);
91
- const previousContentRef = useRef({ android: '', ios: '' });
92
-
93
- // Update content when initialContent prop changes (for edit mode)
94
- // This should only run when loading a template for editing, NOT during active editing
95
- useEffect(() => {
96
- const newAndroidContent = initialContent?.[ANDROID];
97
- const newIosContent = initialContent?.[IOS];
98
-
99
- // Check if this is meaningful initialContent (has actual content)
100
- const hasMeaningfulContent = (newAndroidContent && newAndroidContent.trim() !== '')
101
- || (newIosContent && newIosContent.trim() !== '');
102
-
103
- // Check if we're transitioning from empty to meaningful content (library mode scenario)
104
- const wasEmpty = (!previousContentRef.current.android || previousContentRef.current.android.trim() === '')
105
- && (!previousContentRef.current.ios || previousContentRef.current.ios.trim() === '');
106
- const isTransitioningToContent = wasEmpty && hasMeaningfulContent;
107
-
108
- // Only update if:
109
- // 1. We haven't loaded initial content yet (first load), OR
110
- // 2. We're transitioning from empty to meaningful content (library mode data fetch)
111
- // This prevents the effect from overriding user edits during active editing
112
- // while still allowing content to load properly in library mode
113
- if (!initialContentLoadedRef.current || isTransitioningToContent) {
114
- if (hasMeaningfulContent) {
115
- // Mark as loaded to prevent future updates from overriding user edits
116
- initialContentLoadedRef.current = true;
117
-
118
- setDeviceContent((prev) => {
119
- let updated = false;
120
- const updatedContent = { ...prev };
121
-
122
- if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
123
- updatedContent[ANDROID] = newAndroidContent;
124
- updated = true;
125
- }
126
- if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
127
- updatedContent[IOS] = newIosContent;
128
- updated = true;
129
- }
130
-
131
- return updated ? updatedContent : prev;
132
- });
133
-
134
- // Update previous content ref
135
- previousContentRef.current = {
136
- android: newAndroidContent || '',
137
- ios: newIosContent || '',
138
- };
139
- }
140
- }
141
- }, [initialContent, ANDROID, IOS]);
142
-
143
192
  // Get current active content
144
- const currentContent = useMemo(() => deviceContent?.[activeDevice] || '', [deviceContent, activeDevice]);
193
+ const currentContent = useMemo(() => {
194
+ return deviceContent[activeDevice] || '';
195
+ }, [deviceContent, activeDevice]);
145
196
 
146
197
  // Update content for current device
147
198
  const updateContent = useCallback((newContent) => {
148
199
  // Validate input
149
- if (typeof newContent !== CONTENT_VALIDATION?.DEFAULT_CONTENT_TYPE) {
200
+ if (typeof newContent !== CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE) {
150
201
  console.warn('useInAppContent: newContent must be a string');
151
202
  return;
152
203
  }
@@ -158,14 +209,14 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
158
209
  // When sync is enabled, update both devices with the same content
159
210
  updatedDeviceContent = {
160
211
  [ANDROID]: newContent,
161
- [IOS]: newContent,
212
+ [IOS]: newContent
162
213
  };
163
214
  } else {
164
215
  // When sync is disabled, update only the current device
165
- setDeviceContent((prev) => {
216
+ setDeviceContent(prev => {
166
217
  updatedDeviceContent = {
167
218
  ...prev,
168
- [activeDevice]: newContent,
219
+ [activeDevice]: newContent
169
220
  };
170
221
  return updatedDeviceContent;
171
222
  });
@@ -174,9 +225,8 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
174
225
  setIsDirty(true);
175
226
  changeTimestampRef.current = Date.now();
176
227
 
177
- // Trigger onChange callback - pass only changed device content
178
- // This prevents parent from updating both device states when only one changed
179
- onChange?.({ [activeDevice]: newContent }, activeDevice);
228
+ // Trigger onChange callback with optional chaining
229
+ onChange?.(updatedDeviceContent, activeDevice);
180
230
 
181
231
  // Setup auto-save for independent mode
182
232
  if (autoSave && autoSaveInterval > AUTO_SAVE_CONFIG.MIN_AUTO_SAVE_INTERVAL_MS && newContent.length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH) {
@@ -207,7 +257,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
207
257
  setIsDirty(true);
208
258
  changeTimestampRef.current = Date.now();
209
259
 
210
- // Trigger onChange callback - in sync mode, pass full content
260
+ // Trigger onChange callback with optional chaining
211
261
  onChange?.(updatedDeviceContent, activeDevice);
212
262
 
213
263
  // Setup auto-save
@@ -251,7 +301,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
251
301
  const currentActiveContent = deviceContent[activeDevice];
252
302
  const syncedContent = {
253
303
  [ANDROID]: currentActiveContent,
254
- [IOS]: currentActiveContent,
304
+ [IOS]: currentActiveContent
255
305
  };
256
306
 
257
307
  setDeviceContent(syncedContent);
@@ -275,16 +325,22 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
275
325
  }, [deviceContent, onSave]);
276
326
 
277
327
  // Check if content exists for current device
278
- const hasContent = useMemo(() => typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE
279
- && currentContent?.trim()?.length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH, [currentContent]);
328
+ const hasContent = useMemo(() => {
329
+ return typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE &&
330
+ currentContent.trim().length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH;
331
+ }, [currentContent]);
280
332
 
281
333
  // Get content size for current device
282
- const getContentSize = useCallback(() => typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE
283
- ? currentContent?.length
284
- : CONTENT_VALIDATION?.MIN_CONTENT_LENGTH, [currentContent]);
334
+ const getContentSize = useCallback(() => {
335
+ return typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE
336
+ ? currentContent.length
337
+ : CONTENT_VALIDATION.MIN_CONTENT_LENGTH;
338
+ }, [currentContent]);
285
339
 
286
340
  // Get content for specific device
287
- const getDeviceContent = useCallback((device) => deviceContent[device] || '', [deviceContent]);
341
+ const getDeviceContent = useCallback((device) => {
342
+ return deviceContent[device] || '';
343
+ }, [deviceContent]);
288
344
 
289
345
  // Set content for specific device
290
346
  const setDeviceContent_ = useCallback((device, content) => {
@@ -304,23 +360,25 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
304
360
  // Update both devices when sync is enabled
305
361
  setDeviceContent({
306
362
  [ANDROID]: content,
307
- [IOS]: content,
363
+ [IOS]: content
308
364
  });
309
365
  } else {
310
366
  // Update specific device
311
- setDeviceContent((prev) => ({
367
+ setDeviceContent(prev => ({
312
368
  ...prev,
313
- [device]: content,
369
+ [device]: content
314
370
  }));
315
371
  }
316
372
  setIsDirty(true);
317
373
  }, [keepContentSame, ANDROID, IOS]);
318
374
 
319
375
  // Cleanup on unmount
320
- useEffect(() => () => {
321
- if (autoSaveTimerRef.current) {
322
- clearTimeout(autoSaveTimerRef.current);
323
- }
376
+ useEffect(() => {
377
+ return () => {
378
+ if (autoSaveTimerRef.current) {
379
+ clearTimeout(autoSaveTimerRef.current);
380
+ }
381
+ };
324
382
  }, []);
325
383
 
326
384
  return {
@@ -344,6 +402,6 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
344
402
  toggleContentSync,
345
403
 
346
404
  // Save management
347
- markAsSaved,
405
+ markAsSaved
348
406
  };
349
407
  };
@@ -3,9 +3,7 @@
3
3
  * Manages real-time validation and error display
4
4
  */
5
5
 
6
- import {
7
- useState, useEffect, useCallback, useRef,
8
- } from 'react';
6
+ import { useState, useEffect, useCallback, useRef } from 'react';
9
7
  import { validateHTML, extractAndValidateCSS } from '../utils/htmlValidator';
10
8
  import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/contentSanitizer';
11
9
 
@@ -18,46 +16,12 @@ import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/content
18
16
  * @param {Function} formatValidatorMessage - Message formatter function for validator internationalization
19
17
  * @returns {Object} Validation state and methods
20
18
  */
21
- /**
22
- * Get line number for a character position in text
23
- */
24
- const getLineNumberFromPosition = (text, position) => {
25
- if (position === undefined || position < 0) return 1;
26
- return text.substring(0, position).split('\n').length;
27
- };
28
-
29
- /**
30
- * Find line number for a tag or pattern in content
31
- * Used for API errors to locate where the error occurs
32
- */
33
- const findLineNumberForTag = (content, tagName) => {
34
- if (!content || !tagName) return null;
35
-
36
- // Try to find the tag in the content
37
- // Look for patterns like {{ tagName }}, {{tagName}}, etc.
38
- const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
- const patterns = [
40
- new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g'),
41
- new RegExp(`\\{%[^%]*${escapedTagName}[^%]*%\\}`, 'g'),
42
- ];
43
-
44
- for (const pattern of patterns) {
45
- const match = pattern.exec(content);
46
- if (match) {
47
- return getLineNumberFromPosition(content, match.index);
48
- }
49
- }
50
-
51
- return null;
52
- };
53
-
54
19
  export const useValidation = (content, variant = 'email', options = {}, formatSanitizerMessage = null, formatValidatorMessage = null) => {
55
20
  const {
56
21
  enableRealTime = true,
57
22
  debounceMs = 500,
58
23
  enableSanitization = true,
59
- securityLevel = 'standard',
60
- apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent
24
+ securityLevel = 'standard'
61
25
  } = options;
62
26
 
63
27
  // Validation state
@@ -78,8 +42,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
78
42
  totalErrors: 0,
79
43
  totalWarnings: 0,
80
44
  totalInfo: 0,
81
- hasSecurityIssues: false,
82
- },
45
+ hasSecurityIssues: false
46
+ }
83
47
  });
84
48
 
85
49
  // Refs for debouncing
@@ -91,7 +55,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
91
55
  */
92
56
  const performValidation = useCallback(async (htmlContent) => {
93
57
  if (!htmlContent) {
94
- setValidationState((prev) => ({
58
+ setValidationState(prev => ({
95
59
  ...prev,
96
60
  isValidating: false,
97
61
  htmlErrors: [],
@@ -108,13 +72,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
108
72
  totalErrors: 0,
109
73
  totalWarnings: 0,
110
74
  totalInfo: 0,
111
- hasSecurityIssues: false,
112
- },
75
+ hasSecurityIssues: false
76
+ }
113
77
  }));
114
78
  return;
115
79
  }
116
80
 
117
- setValidationState((prev) => ({ ...prev, isValidating: true }));
81
+ setValidationState(prev => ({ ...prev, isValidating: true }));
118
82
 
119
83
  try {
120
84
  // 1. HTML Validation
@@ -157,12 +121,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
157
121
  totalErrors,
158
122
  totalWarnings,
159
123
  totalInfo,
160
- hasSecurityIssues,
161
- },
124
+ hasSecurityIssues
125
+ }
162
126
  });
127
+
163
128
  } catch (error) {
164
129
  console.error('Validation error:', error);
165
- setValidationState((prev) => ({
130
+ setValidationState(prev => ({
166
131
  ...prev,
167
132
  isValidating: false,
168
133
  htmlErrors: [{
@@ -172,13 +137,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
172
137
  column: 1,
173
138
  rule: 'validation-error',
174
139
  severity: 'error',
175
- source: 'validator',
140
+ source: 'validator'
176
141
  }],
177
142
  isValid: false,
178
143
  summary: {
179
144
  ...prev.summary,
180
- totalErrors: 1,
181
- },
145
+ totalErrors: 1
146
+ }
182
147
  }));
183
148
  }
184
149
  }, [variant, enableSanitization, securityLevel, formatSanitizerMessage, formatValidatorMessage]);
@@ -243,8 +208,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
243
208
  totalErrors: 0,
244
209
  totalWarnings: 0,
245
210
  totalInfo: 0,
246
- hasSecurityIssues: false,
247
- },
211
+ hasSecurityIssues: false
212
+ }
248
213
  });
249
214
  }, []);
250
215
 
@@ -258,10 +223,10 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
258
223
  ...validationState.htmlInfo,
259
224
  ...validationState.cssErrors,
260
225
  ...validationState.cssWarnings,
261
- ...validationState.cssInfo,
226
+ ...validationState.cssInfo
262
227
  ];
263
228
 
264
- return allErrors.filter((error) => error.severity === severity);
229
+ return allErrors.filter(error => error.severity === severity);
265
230
  }, [validationState]);
266
231
 
267
232
  /**
@@ -274,95 +239,32 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
274
239
  ...validationState.htmlInfo,
275
240
  ...validationState.cssErrors,
276
241
  ...validationState.cssWarnings,
277
- ...validationState.cssInfo,
242
+ ...validationState.cssInfo
278
243
  ];
279
244
 
280
- return allErrors.filter((error) => error.source === source);
245
+ return allErrors.filter(error => error.source === source);
281
246
  }, [validationState]);
282
247
 
283
- /**
284
- * Extract line number from error message if present
285
- * API errors might contain line numbers in messages like "Error at line 5" or "Line 10: error"
286
- * Also tries to find line number by searching for the problematic tag in content
287
- */
288
- const extractLineNumberFromMessage = useCallback((message) => {
289
- if (!message || typeof message !== 'string') {
290
- return null;
291
- }
292
- // Try to match patterns like "line 5", "Line 10", "at line 15", "line: 20", etc.
293
- const lineMatch = message.match(/\b(?:line|Line|LINE)\s*:?\s*(\d+)\b/i);
294
- if (lineMatch && lineMatch[1]) {
295
- return parseInt(lineMatch[1], 10);
296
- }
297
-
298
- // Try to extract tag name from error message (e.g., "Unsupported tags: test" -> "test")
299
- const tagMatch = message.match(/(?:tag|tags|Tag|Tags)[\s:]+([a-zA-Z_][a-zA-Z0-9_.]*)/i);
300
- if (tagMatch && tagMatch[1] && content) {
301
- const tagName = tagMatch[1];
302
- const lineNumber = findLineNumberForTag(content, tagName);
303
- if (lineNumber) {
304
- return lineNumber;
305
- }
306
- }
307
-
308
- return null;
309
- }, [content]);
310
-
311
248
  /**
312
249
  * Get all errors and warnings combined
313
- * Includes both client-side validation errors and API validation errors
314
250
  */
315
251
  const getAllIssues = useCallback(() => {
316
- // Convert API validation errors to validation issue format
317
- // IMPORTANT: Only errors from liquidValidation and extractTags endpoints should be in liquidErrors
318
- // These will appear in the "Liquid Issues" tab in ValidationErrorDisplay
319
- // Other errors (like empty content) should be in standardErrors and appear in HTML/Label Issues tabs
320
- const apiLiquidErrors = (apiValidationErrors?.liquidErrors || []).map((errorMessage) => {
321
- const extractedLine = extractLineNumberFromMessage(errorMessage);
322
- return {
323
- type: 'error',
324
- message: errorMessage,
325
- line: extractedLine, // Try to extract line number from message, null if not found
326
- column: null,
327
- rule: 'liquid-api-validation',
328
- severity: 'error',
329
- source: 'liquid-validator', // This source ensures errors appear in "Liquid Issues" tab
330
- };
331
- });
332
-
333
- // Standard errors (non-liquid endpoint errors) appear in HTML/Label Issues tabs
334
- const apiStandardErrors = (apiValidationErrors?.standardErrors || []).map((errorMessage) => {
335
- const extractedLine = extractLineNumberFromMessage(errorMessage);
336
- return {
337
- type: 'error',
338
- message: errorMessage,
339
- line: extractedLine, // Try to extract line number from message, null if not found
340
- column: null,
341
- rule: 'standard-api-validation',
342
- severity: 'error',
343
- source: 'api-validator', // This source ensures errors appear in HTML/Label Issues tabs
344
- };
345
- });
346
-
347
- const allIssues = [
348
- ...(validationState.htmlErrors || []),
349
- ...(validationState.htmlWarnings || []),
350
- ...(validationState.htmlInfo || []),
351
- ...(validationState.cssErrors || []),
352
- ...(validationState.cssWarnings || []),
353
- ...(validationState.cssInfo || []),
354
- ...(validationState.securityIssues || []).map((issue) => ({
252
+ return [
253
+ ...validationState.htmlErrors,
254
+ ...validationState.htmlWarnings,
255
+ ...validationState.htmlInfo,
256
+ ...validationState.cssErrors,
257
+ ...validationState.cssWarnings,
258
+ ...validationState.cssInfo,
259
+ ...validationState.securityIssues.map(issue => ({
355
260
  type: 'error',
356
261
  message: `Security issue: ${issue.type}`,
357
262
  line: 1,
358
263
  column: 1,
359
264
  rule: 'security-violation',
360
265
  severity: 'error',
361
- source: 'security',
362
- })),
363
- // Add API validation errors
364
- ...apiLiquidErrors,
365
- ...apiStandardErrors,
266
+ source: 'security'
267
+ }))
366
268
  ].sort((a, b) => {
367
269
  // Sort by severity (error > warning > info) then by line number
368
270
  const severityOrder = { error: 0, warning: 1, info: 2 };
@@ -371,22 +273,16 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
371
273
  }
372
274
  return (a.line || 0) - (b.line || 0);
373
275
  });
374
-
375
- // Ensure we always return an array
376
- return Array.isArray(allIssues) ? allIssues : [];
377
- }, [validationState, apiValidationErrors, extractLineNumberFromMessage]);
276
+ }, [validationState]);
378
277
 
379
278
  /**
380
279
  * Check if validation is clean (no errors or warnings)
381
- * Includes API validation errors in the check
382
280
  */
383
281
  const isClean = useCallback(() => {
384
- const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
385
- return validationState.summary.totalErrors === 0
386
- && validationState.summary.totalWarnings === 0
387
- && !validationState.summary.hasSecurityIssues
388
- && !hasApiErrors;
389
- }, [validationState.summary, apiValidationErrors]);
282
+ return validationState.summary.totalErrors === 0 &&
283
+ validationState.summary.totalWarnings === 0 &&
284
+ !validationState.summary.hasSecurityIssues;
285
+ }, [validationState.summary]);
390
286
 
391
287
  // Effect to validate content when it changes
392
288
  useEffect(() => {
@@ -396,15 +292,14 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
396
292
  }, [content, validateContent, enableRealTime]);
397
293
 
398
294
  // Cleanup on unmount
399
- useEffect(() => () => {
400
- if (debounceRef.current) {
401
- clearTimeout(debounceRef.current);
402
- }
295
+ useEffect(() => {
296
+ return () => {
297
+ if (debounceRef.current) {
298
+ clearTimeout(debounceRef.current);
299
+ }
300
+ };
403
301
  }, []);
404
302
 
405
- // Include API validation errors in computed properties
406
- const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
407
-
408
303
  return {
409
304
  // Validation state
410
305
  ...validationState,
@@ -421,10 +316,10 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
421
316
  isClean,
422
317
 
423
318
  // Computed properties
424
- hasErrors: validationState.summary.totalErrors > 0 || hasApiErrors,
319
+ hasErrors: validationState.summary.totalErrors > 0,
425
320
  hasWarnings: validationState.summary.totalWarnings > 0,
426
- hasSecurityIssues: validationState.summary.hasSecurityIssues,
321
+ hasSecurityIssues: validationState.summary.hasSecurityIssues
427
322
  };
428
323
  };
429
324
 
430
- export default useValidation;
325
+ export default useValidation;
@@ -26,4 +26,4 @@ export { HTML_EDITOR_VARIANTS, DEVICE_TYPES } from './constants';
26
26
  * - Default export (lazy): ~0KB initial bundle (loads ~400KB when needed)
27
27
  * - HTMLEditorSync: ~400KB added to initial bundle
28
28
  * - Constants: ~1KB added to initial bundle
29
- */
29
+ */