@capillarytech/creatives-library 8.0.292-alpha.11 → 8.0.292-alpha.13

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.292-alpha.11",
4
+ "version": "8.0.292-alpha.13",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -113,6 +113,8 @@ const HTMLEditor = forwardRef(({
113
113
  const mainEditorRef = useRef(null);
114
114
  const modalEditorRef = useRef(null);
115
115
  const [isFullscreenModalOpen, setIsFullscreenModalOpen] = useState(false);
116
+ // InApp only: last initialContent value we applied (by value) to avoid reverting user edits on parent re-render
117
+ const inAppLastAppliedInitialRef = useRef({ android: '', ios: '' });
116
118
 
117
119
  // Get the currently active editor ref based on fullscreen state
118
120
  const getActiveEditorRef = useCallback(() => isFullscreenModalOpen ? modalEditorRef : mainEditorRef, [isFullscreenModalOpen]);
@@ -174,19 +176,22 @@ const HTMLEditor = forwardRef(({
174
176
  const contentToUpdate = typeof initialContent === 'string'
175
177
  ? { [DEVICE_TYPES.ANDROID]: initialContent, [DEVICE_TYPES.IOS]: initialContent }
176
178
  : initialContent;
177
- if (inAppContent.updateContent) {
178
- const activeDevice = inAppContent.activeDevice;
179
- const currentContent = inAppContent.getDeviceContent?.(activeDevice);
180
- // Use active device's value only; empty string is valid (don't fall back to other device)
181
- const newContent = activeDevice in contentToUpdate
182
- ? (contentToUpdate[activeDevice] ?? '')
183
- : (contentToUpdate[DEVICE_TYPES.ANDROID] ?? '');
184
- if (currentContent !== newContent) {
185
- inAppContent.updateContent(newContent, true);
186
- }
187
- }
179
+ const newAndroid = contentToUpdate[DEVICE_TYPES.ANDROID] ?? '';
180
+ const newIos = contentToUpdate[DEVICE_TYPES.IOS] ?? '';
181
+ const last = inAppLastAppliedInitialRef.current;
182
+ // InApp: only sync when initialContent value actually changed (first load or template switch)
183
+ // so we don't revert user typing when parent re-renders with same content (new ref)
184
+ const valueChanged = newAndroid !== (last.android ?? '') || newIos !== (last.ios ?? '');
185
+ if (!valueChanged) return;
186
+ if (!inAppContent.updateContent) return;
187
+ const activeDevice = inAppContent.activeDevice;
188
+ const newContent = activeDevice in contentToUpdate
189
+ ? (contentToUpdate[activeDevice] ?? '')
190
+ : (contentToUpdate[DEVICE_TYPES.ANDROID] ?? '');
191
+ inAppContent.updateContent(newContent, true);
192
+ inAppLastAppliedInitialRef.current = { android: newAndroid, ios: newIos };
188
193
  }
189
- }, [initialContent, isEmailVariant, isInAppVariant]);
194
+ }, [initialContent, isEmailVariant, isInAppVariant, emailContent, inAppContent]);
190
195
  // Handle context change for tag API calls
191
196
  // If variant is INAPP, use SMS layout; otherwise use the channel (EMAIL)
192
197
  const handleContextChange = useCallback((contextData) => {
@@ -300,10 +305,13 @@ const HTMLEditor = forwardRef(({
300
305
  apiValidationErrors, // Pass API validation errors to merge with client-side validation
301
306
  }, formatSanitizerMessage, formatValidatorMessage);
302
307
 
303
- // Expose validation and content state via ref
308
+ // Expose validation and content state via ref (InApp: also expose getDeviceContent for Test and Preview)
304
309
  useImperativeHandle(ref, () => ({
305
310
  getValidation: () => validation,
306
311
  getContent: () => currentContent,
312
+ getDeviceContent: variant === HTML_EDITOR_VARIANTS.INAPP && typeof getDeviceContent === 'function'
313
+ ? getDeviceContent
314
+ : undefined,
307
315
  isContentEmpty: () => !currentContent || currentContent.trim() === '',
308
316
  getIssueCounts: () => {
309
317
  if (!validation || typeof validation.getAllIssues !== 'function') {
@@ -319,7 +327,7 @@ const HTMLEditor = forwardRef(({
319
327
  : countIssuesBySeverity(validation.getAllIssues()),
320
328
  })
321
329
  ,
322
- }), [validation, currentContent, apiValidationErrors]); // Include apiValidationErrors so ref methods return updated counts
330
+ }), [validation, currentContent, apiValidationErrors, variant, getDeviceContent]);
323
331
 
324
332
  // Use ref to store callback to avoid infinite loops (callback in deps would cause re-runs)
325
333
  const onValidationChangeRef = useRef(onValidationChange);
@@ -827,5 +835,5 @@ HTMLEditor.defaultProps = {
827
835
  apiValidationErrors: null, // API validation errors from validateLiquidTemplateContent
828
836
  };
829
837
 
830
- // Export with injectIntl - HTMLEditor now uses forwardRef internally
831
- export default injectIntl(HTMLEditor);
838
+ // Export with injectIntl - forwardRef so parent (e.g. InApp) can use ref for getDeviceContent (Test and Preview)
839
+ export default injectIntl(HTMLEditor, { forwardRef: true });
@@ -95,52 +95,51 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
95
95
  const previousContentRef = useRef({ android: '', ios: '' });
96
96
 
97
97
  // Update content when initialContent prop changes (for edit mode)
98
- // This should only run when loading a template for editing, NOT during active editing
98
+ // Only update when: first load, empty->content transition, or template switch (value changed).
99
+ // Value-based comparison prevents reverting user edits when parent re-renders with same content (new ref).
99
100
  useEffect(() => {
100
101
  const newAndroidContent = initialContent?.[ANDROID];
101
102
  const newIosContent = initialContent?.[IOS];
103
+ const prevRef = previousContentRef.current;
102
104
 
103
- // Check if this is meaningful initialContent (has actual content)
104
105
  const hasMeaningfulContent = (newAndroidContent && newAndroidContent.trim() !== '')
105
106
  || (newIosContent && newIosContent.trim() !== '');
106
107
 
107
- // Check if we're transitioning from empty to meaningful content (library mode scenario)
108
- const wasEmpty = (!previousContentRef.current.android || previousContentRef.current.android.trim() === '')
109
- && (!previousContentRef.current.ios || previousContentRef.current.ios.trim() === '');
108
+ const wasEmpty = (!prevRef.android || prevRef.android.trim() === '')
109
+ && (!prevRef.ios || prevRef.ios.trim() === '');
110
110
  const isTransitioningToContent = wasEmpty && hasMeaningfulContent;
111
111
 
112
- // Only update if:
113
- // 1. We haven't loaded initial content yet (first load), OR
114
- // 2. We're transitioning from empty to meaningful content (library mode data fetch)
115
- // This prevents the effect from overriding user edits during active editing
116
- // while still allowing content to load properly in library mode
117
- if (!initialContentLoadedRef.current || isTransitioningToContent) {
118
- if (hasMeaningfulContent) {
119
- // Mark as loaded to prevent future updates from overriding user edits
120
- initialContentLoadedRef.current = true;
121
-
122
- setDeviceContent((prev) => {
123
- let updated = false;
124
- const updatedContent = { ...prev };
125
-
126
- if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
127
- updatedContent[ANDROID] = newAndroidContent;
128
- updated = true;
129
- }
130
- if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
131
- updatedContent[IOS] = newIosContent;
132
- updated = true;
133
- }
112
+ // Value-based: did initialContent content actually change? (template switch)
113
+ const initialValueChanged = (newAndroidContent || '') !== (prevRef.android || '')
114
+ || (newIosContent || '') !== (prevRef.ios || '');
134
115
 
135
- return updated ? updatedContent : prev;
136
- });
116
+ const shouldSync = !initialContentLoadedRef.current
117
+ || isTransitioningToContent
118
+ || initialValueChanged;
137
119
 
138
- // Update previous content ref
139
- previousContentRef.current = {
140
- android: newAndroidContent || '',
141
- ios: newIosContent || '',
142
- };
143
- }
120
+ if (shouldSync && hasMeaningfulContent) {
121
+ initialContentLoadedRef.current = true;
122
+
123
+ setDeviceContent((prev) => {
124
+ let updated = false;
125
+ const updatedContent = { ...prev };
126
+
127
+ if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
128
+ updatedContent[ANDROID] = newAndroidContent;
129
+ updated = true;
130
+ }
131
+ if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
132
+ updatedContent[IOS] = newIosContent;
133
+ updated = true;
134
+ }
135
+
136
+ return updated ? updatedContent : prev;
137
+ });
138
+
139
+ previousContentRef.current = {
140
+ android: newAndroidContent || '',
141
+ ios: newIosContent || '',
142
+ };
144
143
  }
145
144
  }, [initialContent, ANDROID, IOS]);
146
145
 
@@ -40,14 +40,14 @@ const LazyHTMLEditor = React.lazy(() =>
40
40
  *
41
41
  * This component wraps the HTMLEditor with React.lazy() and Suspense
42
42
  * to enable code splitting and reduce initial bundle size.
43
+ * Forwards ref so parent (e.g. InApp) can call getDeviceContent for Test and Preview.
43
44
  */
44
- const HTMLEditorLazy = (props) => {
45
- return (
46
- <Suspense fallback={<HTMLEditorFallback />}>
47
- <LazyHTMLEditor {...props} />
48
- </Suspense>
49
- );
50
- };
45
+ const HTMLEditorLazy = React.forwardRef((props, ref) => (
46
+ <Suspense fallback={<HTMLEditorFallback />}>
47
+ <LazyHTMLEditor {...props} ref={ref} />
48
+ </Suspense>
49
+ ));
50
+ HTMLEditorLazy.displayName = 'HTMLEditorLazy';
51
51
 
52
52
  // Forward all prop types from the original component
53
53
  HTMLEditorLazy.propTypes = {
@@ -61,7 +61,7 @@ import { validateInAppContent } from "../../utils/commonUtils";
61
61
  import { hasLiquidSupportFeature } from "../../utils/common";
62
62
  import formBuilderMessages from "../../v2Components/FormBuilder/messages";
63
63
  import HTMLEditor from "../../v2Components/HtmlEditor";
64
- import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
64
+ import { HTML_EDITOR_VARIANTS, DEVICE_TYPES as HTML_DEVICE_TYPES } from "../../v2Components/HtmlEditor/constants";
65
65
  import { INAPP_EDITOR_TYPES } from "../InAppWrapper/constants";
66
66
  import InappAdvanced from "../InappAdvance/index";
67
67
  import { ErrorInfoNote } from "../../v2Components/ErrorInfoNote";
@@ -145,6 +145,7 @@ export const InApp = (props) => {
145
145
  // Refs to store latest content before layout changes
146
146
  const htmlContentAndroidRef = useRef(htmlContentAndroid);
147
147
  const htmlContentIosRef = useRef(htmlContentIos);
148
+ const htmlEditorRef = useRef(null);
148
149
  const [accountId, setAccountId] = useState("");
149
150
  const [accessToken, setAccessToken] = useState("");
150
151
  const [accountName, setAccountName] = useState("");
@@ -177,9 +178,19 @@ export const InApp = (props) => {
177
178
  // Transformation to payload structure happens in prepareTestMessagePayload
178
179
  // Reference: Based on getPreviewSection() function (lines 490-530) which prepares content for TemplatePreview
179
180
  const getTemplateContent = useCallback(() => {
180
- // For HTML template, use HTML editor content so preview stays in sync with typing
181
- const androidMsg = isHTMLTemplate ? (htmlContentAndroid ?? '') : templateMessageAndroid;
182
- const iosMsg = isHTMLTemplate ? (htmlContentIos ?? '') : templateMessageIos;
181
+ // For HTML template: prefer editor ref (source of truth) so library mode preview gets latest content
182
+ let androidMsg = templateMessageAndroid;
183
+ let iosMsg = templateMessageIos;
184
+ if (isHTMLTemplate) {
185
+ const getDeviceContentFromEditor = htmlEditorRef.current?.getDeviceContent;
186
+ if (typeof getDeviceContentFromEditor === 'function') {
187
+ androidMsg = getDeviceContentFromEditor(HTML_DEVICE_TYPES.ANDROID) ?? '';
188
+ iosMsg = getDeviceContentFromEditor(HTML_DEVICE_TYPES.IOS) ?? '';
189
+ } else {
190
+ androidMsg = htmlContentAndroidRef.current ?? htmlContentAndroid ?? '';
191
+ iosMsg = htmlContentIosRef.current ?? htmlContentIos ?? '';
192
+ }
193
+ }
183
194
 
184
195
  // Prepare Android content
185
196
  const androidMediaPreview = {};
@@ -1323,6 +1334,7 @@ export const InApp = (props) => {
1323
1334
  )}
1324
1335
  {shouldUseHTMLEditor ? (
1325
1336
  <HTMLEditor
1337
+ ref={htmlEditorRef}
1326
1338
  key={`inapp-html-editor-v${htmlEditorContentVersion}`}
1327
1339
  variant={HTML_EDITOR_VARIANTS.INAPP}
1328
1340
  layoutType={templateLayoutType}
@@ -1,70 +0,0 @@
1
- # DeviceToggle – Click flow and when clicks are blocked
2
-
3
- ## 1. Click flow (button → end)
4
-
5
- 1. **User clicks Android or iOS tab**
6
- - `CapButton` receives the click and runs
7
- - `onClick={() => handleDeviceClick(DEVICE_TYPES.ANDROID)}` or
8
- - `onClick={() => handleDeviceClick(DEVICE_TYPES.IOS)}`.
9
-
10
- 2. **`handleDeviceClick(device)`** (in `DeviceToggle/index.js`):
11
- - Only does something if **both**:
12
- - `onDeviceChange` is truthy (passed from parent), and
13
- - `device !== activeDevice` (clicked tab is not already active).
14
- - If both hold: calls `onDeviceChange(device)`.
15
-
16
- 3. **Parent wiring** (`HTMLEditor.js`):
17
- - `onDeviceChange={switchDevice}`
18
- - So `onDeviceChange` is the `switchDevice` from `useInAppContent`.
19
-
20
- 4. **`switchDevice(device)`** (`hooks/useInAppContent.js`):
21
- - `validDevices = [ANDROID, IOS]` (from `DEVICE_TYPES`: `'android'`, `'ios'`).
22
- - If `device !== activeDevice` and `validDevices.includes(device)` → `setActiveDevice(device)`.
23
- - If `device` is invalid → logs a warning and does nothing.
24
-
25
- 5. **Result**
26
- - `activeDevice` updates → DeviceToggle and preview re-render with the new device; no explicit “block” inside this flow.
27
-
28
- ---
29
-
30
- ## 2. When the click is effectively “blocked” or does nothing
31
-
32
- - **No handler**
33
- If the parent does not pass `onDeviceChange` (or passes `undefined`/`null`), `handleDeviceClick` never calls anything. The button still receives the click; there is no visual “disabled” in DeviceToggle.
34
-
35
- - **Same device**
36
- If the user clicks the already-active tab (`device === activeDevice`), `handleDeviceClick` does nothing by design.
37
-
38
- - **Invalid device**
39
- If `device` is not `'android'` or `'ios'` (e.g. wrong constant when using the repo as a library), `switchDevice` only warns and does not update state.
40
-
41
- - **Click never reaches the button (layout/CSS)**
42
- Another element is on top of the tab and captures the click (see below). This is the likely cause when “the iOS button is not clickable in edit mode” when using the repo as a library.
43
-
44
- ---
45
-
46
- ## 3. Why the iOS button can be unclickable in library (edit) mode
47
-
48
- In **library mode** (`isFullMode === false`), InApp uses a special layout:
49
-
50
- - **Header**: flex row with **DeviceToggle** first and **EditorToolbar** second.
51
- - **DeviceToggle** has `flex: 1` (takes remaining space).
52
- - **EditorToolbar** gets `position: absolute` (no `top`/`right`/`left`), so it is taken out of flow.
53
-
54
- Because the toolbar is out of flow, the only flex child in flow is the DeviceToggle, so it expands to the full width of the header. The absolutely positioned toolbar is then drawn at its “static” position, which is the **right side** of the header. So the toolbar sits **on top of the right part of the header**, which is exactly where the **iOS** tab and the “Keep content same” checkbox are. Clicks on the iOS tab are then hit by the toolbar first, so the **iOS button appears not clickable** even though the handler logic is fine.
55
-
56
- So the problem is **not** that the button is disabled or that the click handler is blocked; it’s that **the EditorToolbar overlaps the iOS tab in library mode** and steals the click.
57
-
58
- ---
59
-
60
- ## 4. Recommended fix (library mode)
61
-
62
- Ensure the toolbar does not overlap the DeviceToggle:
63
-
64
- - Either **remove** the library-mode override that sets `position: absolute` on the toolbar so the header stays in normal flex flow, **or**
65
- - If you keep `position: absolute` for the toolbar:
66
- - Give the header a **positioning context** (e.g. `position: relative`).
67
- - Pin the toolbar to the right (e.g. `right: 0; top: 0`).
68
- - Reserve space for the toolbar so DeviceToggle doesn’t extend under it (e.g. `padding-right` on the header or a fixed/min width for the toolbar and corresponding margin on the DeviceToggle).
69
-
70
- With that, the DeviceToggle (including the iOS tab) remains fully clickable in edit/library mode.