@capillarytech/creatives-library 8.0.302 → 8.0.304

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.
@@ -45,7 +45,6 @@ export const GIFT_CARDS = 'GIFT_CARDS';
45
45
  export const PROMO_ENGINE = 'PROMO_ENGINE';
46
46
  export const LIQUID_SUPPORT = 'ENABLE_LIQUID_SUPPORT';
47
47
  export const ENABLE_NEW_MPUSH = 'ENABLE_NEW_MPUSH';
48
- export const ENABLE_NEW_EDITOR_FLOW_INAPP = 'ENABLE_NEW_EDITOR_FLOW_INAPP';
49
48
  export const SUPPORT_CK_EDITOR = 'SUPPORT_CK_EDITOR';
50
49
  export const CUSTOM_TAG = 'CustomTagMessage';
51
50
  export const CUSTOMER_EXTENDED_FIELD = 'Customer extended fields';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.302",
4
+ "version": "8.0.304",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -24,8 +24,7 @@ import {
24
24
  ENABLE_WEBPUSH,
25
25
  LIQUID_SUPPORT,
26
26
  SUPPORT_CK_EDITOR,
27
- ENABLE_NEW_MPUSH,
28
- ENABLE_NEW_EDITOR_FLOW_INAPP
27
+ ENABLE_NEW_MPUSH
29
28
  } from '../constants/unified';
30
29
  import { apiMessageFormatHandler } from './commonUtils';
31
30
 
@@ -143,11 +142,6 @@ export const hasNewMobilePushFeatureEnabled = Auth.hasFeatureAccess.bind(
143
142
  ENABLE_NEW_MPUSH,
144
143
  );
145
144
 
146
- export const hasNewEditorFlowInAppEnabled = Auth.hasFeatureAccess.bind(
147
- null,
148
- ENABLE_NEW_EDITOR_FLOW_INAPP,
149
- );
150
-
151
145
  //filtering tags based on scope
152
146
  export const filterTags = (tagsToFilter, tagsList) => tagsList?.filter(
153
147
  (tag) => !tagsToFilter?.includes(tag?.definition?.value)
@@ -63,7 +63,6 @@ const CapDeviceContent = (props) => {
63
63
  deepLinkValue,
64
64
  setDeepLinkValue,
65
65
  onCopyTitleAndContent,
66
- isOtherDeviceSupported,
67
66
  tags,
68
67
  onTagSelect,
69
68
  handleOnTagsContextChange,
@@ -168,15 +167,13 @@ const CapDeviceContent = (props) => {
168
167
  return (
169
168
  <>
170
169
  <CapRow className="creatives-device-content">
171
- {isOtherDeviceSupported && (
172
- <CapLink
173
- title={isAndroid
174
- ? formatMessage(messages.copyContentFromIOS)
175
- : formatMessage(messages.copyCotentFromAndroid)}
176
- className="inapp-copy-content"
177
- onClick={onCopyTitleAndContent}
178
- />
179
- )}
170
+ <CapLink
171
+ title={isAndroid
172
+ ? formatMessage(messages.copyContentFromIOS)
173
+ : formatMessage(messages.copyCotentFromAndroid)}
174
+ className="inapp-copy-content"
175
+ onClick={onCopyTitleAndContent}
176
+ />
180
177
  <CapRow className="creatives-inapp-title">
181
178
  <CapColumn
182
179
  className="inapp-content-main"
@@ -214,9 +214,6 @@
214
214
  }
215
215
 
216
216
  .common-test-and-preview-slidebox {
217
- .common-test-preview-modal-wrap {
218
- z-index: 10003;
219
- }
220
217
 
221
218
  .ant-modal-mask,
222
219
  .ant-modal-wrap {
@@ -40,11 +40,6 @@ function BeePopupEditor(props) {
40
40
  const savedCallback = useRef();
41
41
  const beeInstanceRef = useRef(null);
42
42
  const isInitializedRef = useRef(false);
43
- const beeJsonRef = useRef(beeJson);
44
-
45
- useEffect(() => {
46
- beeJsonRef.current = beeJson;
47
- }, [beeJson]);
48
43
 
49
44
  const [visibleTaglist, setVisibleTaglist] = useState(false);
50
45
  const [selectedTag, setSelectedTag] = useState({});
@@ -116,10 +111,8 @@ function BeePopupEditor(props) {
116
111
  window.BeePlugin.create(tokenData, beeConfig, (instance) => {
117
112
  beePluginInstance = instance;
118
113
  beeInstanceRef.current = instance;
119
- // Use ref to get the latest beeJson the closure captures the value at effect time,
120
- // but beeJsonRef.current is always up-to-date (e.g. when template data loads async)
121
- const latestBeeJson = beeJsonRef.current;
122
- const parseJson = typeof latestBeeJson === 'string' ? JSON.parse(latestBeeJson) : latestBeeJson;
114
+ // Check if beeJson is already an object (happens when layout type changes)
115
+ const parseJson = typeof beeJson === 'string' ? JSON.parse(beeJson) : beeJson;
123
116
  beePluginInstance.start(parseJson);
124
117
  saveBeeInstance(beePluginInstance, device);
125
118
  isInitializedRef.current = true;
@@ -1078,38 +1078,8 @@ export function SlideBoxContent(props) {
1078
1078
  )}
1079
1079
 
1080
1080
  {isCreateInApp && (
1081
- (isFullMode && !commonUtil.hasNewEditorFlowInAppEnabled()) ||
1082
- (!isFullMode && isLoyaltyModule) ||
1083
- (!isFullMode && !isLoyaltyModule && !commonUtil.hasNewEditorFlowInAppEnabled()) ? (
1084
- <InApp
1085
- key="creatives-inapp-create"
1086
- location={{ pathname: '/inapp/create', query, search: '' }}
1087
- setIsLoadingContent={setIsLoadingContent}
1088
- isGetFormData={isGetFormData}
1089
- getFormData={getFormData}
1090
- getDefaultTags={type}
1091
- isFullMode={isFullMode}
1092
- templateData={templateData}
1093
- cap={cap}
1094
- showTemplateName={showTemplateName}
1095
- showLiquidErrorInFooter={showLiquidErrorInFooter}
1096
- onValidationFail={onValidationFail}
1097
- forwardedTags={forwardedTags}
1098
- selectedOfferDetails={selectedOfferDetails}
1099
- onPreviewContentClicked={onPreviewContentClicked}
1100
- onTestContentClicked={onTestContentClicked}
1101
- eventContextTags={eventContextTags}
1102
- onCreateComplete={onCreateComplete}
1103
- handleClose={handleClose}
1104
- moduleType={moduleType}
1105
- showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
1106
- handleTestAndPreview={handleTestAndPreview}
1107
- handleCloseTestAndPreview={handleCloseTestAndPreview}
1108
- isTestAndPreviewMode={isTestAndPreviewMode}
1109
- />
1110
- ) : (
1111
- <InAppWrapper
1112
- key="creatives-inapp-wrapper"
1081
+ <InAppWrapper
1082
+ key="creatives-inapp-wrapper"
1113
1083
  date={new Date().getMilliseconds()}
1114
1084
  setIsLoadingContent={setIsLoadingContent}
1115
1085
  onInAppEditorTypeChange={onInAppEditorTypeChange}
@@ -1144,12 +1114,10 @@ export function SlideBoxContent(props) {
1144
1114
  handleCloseTestAndPreview={handleCloseTestAndPreview}
1145
1115
  isTestAndPreviewMode={isTestAndPreviewMode}
1146
1116
  />
1147
- )
1148
1117
  )}
1149
-
1118
+
1150
1119
  {isEditInApp && (<InApp
1151
1120
  isFullMode={isFullMode}
1152
- isLoyaltyModule={isLoyaltyModule}
1153
1121
  templateData={templateData}
1154
1122
  getFormData={getFormData}
1155
1123
  getDefaultTags={type}
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
3
+
3
4
  import { SlideBoxContent } from '../SlideBoxContent';
4
5
  import mockdata from '../../mockdata';
5
6
  import { templateDetailsImage, templateDetailsVideo, templateDetailsText, templateDetails_ } from '../../Viber/tests/mockData';
@@ -41,15 +42,6 @@ jest.mock('../../WebPush', () => ({
41
42
  ),
42
43
  }));
43
44
 
44
- jest.mock('v2Containers/InApp', () => () => <div data-test="inapp" />);
45
- jest.mock('v2Containers/InAppWrapper', () => () => <div data-test="inapp-wrapper" />);
46
-
47
- jest.mock('../../../utils/commonUtils', () => ({
48
- hasNewEditorFlowInAppEnabled: jest.fn(),
49
- }));
50
-
51
- import commonUtil from '../../../utils/commonUtils';
52
-
53
45
  describe('Test SlideBoxContent container', () => {
54
46
  const onCreateComplete = jest.fn();
55
47
  let renderedComponent;
@@ -918,64 +910,4 @@ describe('Test SlideBoxContent container', () => {
918
910
  expect(renderedComponent).toMatchSnapshot();
919
911
  });
920
912
  });
921
-
922
- describe('InApp vs InAppWrapper rendering conditions', () => {
923
-
924
- beforeEach(() => {
925
- jest.clearAllMocks();
926
- });
927
-
928
- it('renders InAppWrapper when isFullMode=true and new editor disabled', () => {
929
- commonUtil.hasNewEditorFlowInAppEnabled.mockReturnValue(false);
930
-
931
- renderFunction(
932
- 'INAPP',
933
- 'createTemplate',
934
- { mode: 'create' },
935
- { isFullMode: true, isLoyaltyModule: false }
936
- );
937
-
938
- expect(renderedComponent).toMatchSnapshot();
939
- });
940
-
941
- it('renders InApp when isFullMode=false and loyalty module enabled', () => {
942
- commonUtil.hasNewEditorFlowInAppEnabled.mockReturnValue(false);
943
-
944
- renderFunction(
945
- 'INAPP',
946
- 'createTemplate',
947
- { mode: 'create' },
948
- { isFullMode: false, isLoyaltyModule: true }
949
- );
950
-
951
- expect(renderedComponent).toMatchSnapshot();
952
- });
953
-
954
- it('renders InApp when not full mode, not loyalty and new editor disabled', () => {
955
- commonUtil.hasNewEditorFlowInAppEnabled.mockReturnValue(false);
956
-
957
- renderFunction(
958
- 'INAPP',
959
- 'createTemplate',
960
- { mode: 'create' },
961
- { isFullMode: false, isLoyaltyModule: false }
962
- );
963
-
964
- expect(renderedComponent).toMatchSnapshot();
965
- });
966
-
967
- it('renders InAppWrapper when full mode and new editor enabled', () => {
968
- commonUtil.hasNewEditorFlowInAppEnabled.mockReturnValue(true);
969
-
970
- renderFunction(
971
- 'INAPP',
972
- 'createTemplate',
973
- { mode: 'create' },
974
- { isFullMode: true, isLoyaltyModule: false }
975
- );
976
-
977
- expect(renderedComponent).toMatchSnapshot();
978
- });
979
-
980
- });
981
913
  });
@@ -202,114 +202,6 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
202
202
  </SlideBoxContent__CreativesWrapper>
203
203
  `;
204
204
 
205
- exports[`Test SlideBoxContent container InApp vs InAppWrapper rendering conditions renders InApp when isFullMode=false and loyalty module enabled 1`] = `
206
- <SlideBoxContent__CreativesWrapper>
207
- <Component
208
- getDefaultTags=""
209
- isFullMode={false}
210
- key="creatives-inapp-create"
211
- location={
212
- Object {
213
- "pathname": "/inapp/create",
214
- "query": Object {
215
- "isEditFromCampaigns": undefined,
216
- "module": "library",
217
- "type": "embedded",
218
- },
219
- "search": "",
220
- }
221
- }
222
- onCreateComplete={[MockFunction]}
223
- templateData={
224
- Object {
225
- "mode": "create",
226
- }
227
- }
228
- />
229
- </SlideBoxContent__CreativesWrapper>
230
- `;
231
-
232
- exports[`Test SlideBoxContent container InApp vs InAppWrapper rendering conditions renders InApp when not full mode, not loyalty and new editor disabled 1`] = `
233
- <SlideBoxContent__CreativesWrapper>
234
- <Component
235
- getDefaultTags=""
236
- isFullMode={false}
237
- key="creatives-inapp-create"
238
- location={
239
- Object {
240
- "pathname": "/inapp/create",
241
- "query": Object {
242
- "isEditFromCampaigns": undefined,
243
- "module": "library",
244
- "type": "embedded",
245
- },
246
- "search": "",
247
- }
248
- }
249
- onCreateComplete={[MockFunction]}
250
- templateData={
251
- Object {
252
- "mode": "create",
253
- }
254
- }
255
- />
256
- </SlideBoxContent__CreativesWrapper>
257
- `;
258
-
259
- exports[`Test SlideBoxContent container InApp vs InAppWrapper rendering conditions renders InAppWrapper when full mode and new editor enabled 1`] = `
260
- <SlideBoxContent__CreativesWrapper>
261
- <Component
262
- getDefaultTags=""
263
- isFullMode={true}
264
- key="creatives-inapp-create"
265
- location={
266
- Object {
267
- "pathname": "/inapp/create",
268
- "query": Object {
269
- "isEditFromCampaigns": undefined,
270
- "module": "default",
271
- "type": false,
272
- },
273
- "search": "",
274
- }
275
- }
276
- onCreateComplete={[MockFunction]}
277
- templateData={
278
- Object {
279
- "mode": "create",
280
- }
281
- }
282
- />
283
- </SlideBoxContent__CreativesWrapper>
284
- `;
285
-
286
- exports[`Test SlideBoxContent container InApp vs InAppWrapper rendering conditions renders InAppWrapper when isFullMode=true and new editor disabled 1`] = `
287
- <SlideBoxContent__CreativesWrapper>
288
- <Component
289
- getDefaultTags=""
290
- isFullMode={true}
291
- key="creatives-inapp-create"
292
- location={
293
- Object {
294
- "pathname": "/inapp/create",
295
- "query": Object {
296
- "isEditFromCampaigns": undefined,
297
- "module": "default",
298
- "type": false,
299
- },
300
- "search": "",
301
- }
302
- }
303
- onCreateComplete={[MockFunction]}
304
- templateData={
305
- Object {
306
- "mode": "create",
307
- }
308
- }
309
- />
310
- </SlideBoxContent__CreativesWrapper>
311
- `;
312
-
313
205
  exports[`Test SlideBoxContent container Should handle isTestAndPreviewMode IIFE implementation correctly 1`] = `
314
206
  <SlideBoxContent__CreativesWrapper>
315
207
  <ForwardRef
@@ -1211,26 +1103,17 @@ exports[`Test SlideBoxContent container Should render correct component for what
1211
1103
 
1212
1104
  exports[`Test SlideBoxContent container Should render correct component for whatsapp channel create mode 2`] = `
1213
1105
  <SlideBoxContent__CreativesWrapper>
1214
- <Component
1106
+ <Connect(Connect(UserIsAuthenticated(InjectIntl(CreativesCommon))))
1107
+ date={0}
1215
1108
  getDefaultTags=""
1216
- key="creatives-inapp-create"
1217
- location={
1218
- Object {
1219
- "pathname": "/inapp/create",
1220
- "query": Object {
1221
- "isEditFromCampaigns": undefined,
1222
- "module": "library",
1223
- "type": "embedded",
1224
- },
1225
- "search": "",
1226
- }
1227
- }
1109
+ key="creatives-inapp-wrapper"
1228
1110
  onCreateComplete={[MockFunction]}
1229
1111
  templateData={
1230
1112
  Object {
1231
1113
  "mode": "create",
1232
1114
  }
1233
1115
  }
1116
+ type=""
1234
1117
  />
1235
1118
  </SlideBoxContent__CreativesWrapper>
1236
1119
  `;
@@ -39,58 +39,33 @@ jest.mock('../../../utils/common', () => ({
39
39
  // Mock HTMLEditor component - must be before imports
40
40
  jest.mock('../../../v2Components/HtmlEditor', () => {
41
41
  const React = require('react');
42
-
43
- const MockHTMLEditor = function MockHTMLEditor({
44
- variant,
45
- initialContent,
46
- onContentChange,
47
- onSave,
48
- 'data-test': dataTest,
49
- }) {
50
- return React.createElement(
51
- 'div',
52
- { 'data-testid': dataTest || 'inapp-html-editor' },
42
+ const MockHTMLEditor = function MockHTMLEditor({ variant, initialContent, onContentChange, onSave, 'data-test': dataTest }) {
43
+ return React.createElement('div', { 'data-testid': dataTest || 'html-editor' },
53
44
  React.createElement('div', { 'data-testid': 'html-editor-variant' }, variant),
54
- React.createElement(
55
- 'div',
56
- { 'data-testid': 'html-editor-initial-android' },
57
- initialContent?.android || ''
58
- ),
59
- React.createElement(
60
- 'div',
61
- { 'data-testid': 'html-editor-initial-ios' },
62
- initialContent?.ios || ''
63
- ),
64
- React.createElement(
65
- 'button',
66
- {
67
- 'data-testid': 'html-editor-change-button',
68
- onClick: () => {
69
- if (onContentChange) {
70
- onContentChange({
71
- android: '<p>Updated Android HTML</p>',
72
- ios: '<p>Updated iOS HTML</p>',
73
- });
74
- }
75
- },
76
- },
77
- 'Simulate Content Change'
78
- ),
79
- React.createElement(
80
- 'button',
81
- {
82
- 'data-testid': 'html-editor-save-button',
83
- onClick: () => {
84
- if (onSave) {
85
- onSave({
86
- android: '<p>Saved Android HTML</p>',
87
- ios: '<p>Saved iOS HTML</p>',
88
- });
89
- }
90
- },
91
- },
92
- 'Simulate Save'
93
- )
45
+ React.createElement('div', { 'data-testid': 'html-editor-initial-android' }, initialContent?.android || ''),
46
+ React.createElement('div', { 'data-testid': 'html-editor-initial-ios' }, initialContent?.ios || ''),
47
+ React.createElement('button', {
48
+ 'data-testid': 'html-editor-change-button',
49
+ onClick: () => {
50
+ if (onContentChange) {
51
+ onContentChange({
52
+ android: '<p>Updated Android HTML</p>',
53
+ ios: '<p>Updated iOS HTML</p>',
54
+ });
55
+ }
56
+ }
57
+ }, 'Simulate Content Change'),
58
+ React.createElement('button', {
59
+ 'data-testid': 'html-editor-save-button',
60
+ onClick: () => {
61
+ if (onSave) {
62
+ onSave({
63
+ android: '<p>Saved Android HTML</p>',
64
+ ios: '<p>Saved iOS HTML</p>',
65
+ });
66
+ }
67
+ }
68
+ }, 'Simulate Save')
94
69
  );
95
70
  };
96
71
  MockHTMLEditor.displayName = 'MockHTMLEditor';
@@ -100,25 +75,22 @@ jest.mock('../../../v2Components/HtmlEditor', () => {
100
75
  };
101
76
  });
102
77
 
103
- /**
104
- * Mock CapDeviceContent to ensure HTMLEditor renders
105
- */
106
- jest.mock('../../../v2Components/CapDeviceContent', () => {
107
- const React = require('react');
108
- const HTMLEditor = require('../../../v2Components/HtmlEditor').default;
78
+ import React from 'react';
79
+ import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
80
+ import '@testing-library/jest-dom';
81
+ import { IntlProvider } from 'react-intl';
82
+ import { Provider } from 'react-redux';
83
+ import { configureStore } from '@capillarytech/vulcan-react-sdk/utils';
84
+ import history from '../../../utils/history';
85
+ import { initialReducer } from '../../../initialReducer';
86
+ import { InApp } from '../index';
87
+ import { INAPP_EDITOR_TYPES } from '../../InAppWrapper/constants';
109
88
 
89
+
90
+ // Mock other dependencies
91
+ jest.mock('../../../v2Components/CapDeviceContent', () => {
110
92
  return function MockCapDeviceContent() {
111
- return (
112
- <div data-testid="cap-device-content">
113
- <HTMLEditor
114
- variant="inapp"
115
- initialContent={{ android: '', ios: '' }}
116
- onContentChange={jest.fn()}
117
- onSave={jest.fn()}
118
- data-test="inapp-html-editor"
119
- />
120
- </div>
121
- );
93
+ return <div data-testid="cap-device-content">Device Content</div>;
122
94
  };
123
95
  });
124
96
 
@@ -128,20 +100,7 @@ jest.mock('../../../v2Components/TemplatePreview', () => {
128
100
  };
129
101
  });
130
102
 
131
- import React from 'react';
132
- import { render, screen, fireEvent, act } from '@testing-library/react';
133
- import '@testing-library/jest-dom';
134
- import { IntlProvider } from 'react-intl';
135
- import { Provider } from 'react-redux';
136
- import { configureStore } from '@capillarytech/vulcan-react-sdk/utils';
137
-
138
- import history from '../../../utils/history';
139
- import { initialReducer } from '../../../initialReducer';
140
- import { InApp } from '../index';
141
- import { INAPP_EDITOR_TYPES } from '../../InAppWrapper/constants';
142
-
143
103
  let store;
144
-
145
104
  beforeAll(() => {
146
105
  store = configureStore({}, initialReducer, history);
147
106
  });
@@ -176,6 +135,8 @@ const defaultProps = {
176
135
  configs: {
177
136
  android: '1',
178
137
  ios: '1',
138
+ accessToken: 'test-token',
139
+ deeplink: '{}',
179
140
  },
180
141
  },
181
142
  },
@@ -184,6 +145,7 @@ const defaultProps = {
184
145
  query: {},
185
146
  search: '',
186
147
  },
148
+ getDefaultTags: null,
187
149
  supportedTags: [],
188
150
  metaEntities: {
189
151
  tags: {
@@ -196,11 +158,26 @@ const defaultProps = {
196
158
  currentOrgDetails: {
197
159
  accessibleFeatures: [],
198
160
  },
161
+ fetchingLiquidValidation: false,
162
+ getTemplateDetailsInProgress: false,
163
+ isEditInApp: false,
164
+ };
165
+
166
+ // Create stable defaultData objects outside renderComponent to avoid reference changes
167
+ const createStableDefaultData = (editorType) => {
168
+ if (!editorType) return {};
169
+ return { 'editor-type': editorType };
199
170
  };
200
171
 
201
172
  const renderComponent = (props = {}) => {
202
173
  const mergedProps = { ...defaultProps, ...props };
203
-
174
+ // Ensure defaultData is always an object and stable
175
+ if (!mergedProps.defaultData) {
176
+ mergedProps.defaultData = {};
177
+ } else if (mergedProps.defaultData['editor-type']) {
178
+ // Create a stable reference for defaultData
179
+ mergedProps.defaultData = createStableDefaultData(mergedProps.defaultData['editor-type']);
180
+ }
204
181
  return render(
205
182
  <Provider store={store}>
206
183
  <IntlProvider locale="en" messages={{}}>
@@ -218,33 +195,36 @@ describe('InApp HTMLEditor Integration', () => {
218
195
  describe('Template Name Editing', () => {
219
196
  test('should not truncate template name when editing', async () => {
220
197
  const showTemplateNameMock = jest.fn();
221
-
222
198
  renderComponent({
223
- defaultData: {
224
- 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR,
225
- 'template-name': 'abcd',
226
- },
199
+ defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR, 'template-name': 'abcd' },
227
200
  showTemplateName: showTemplateNameMock,
228
201
  });
229
202
 
230
- const editor = await screen.findByTestId('inapp-html-editor');
231
- expect(editor).toBeInTheDocument();
203
+ // Wait for HTML editor to render
204
+ await waitFor(() => {
205
+ expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
206
+ });
207
+
208
+ // Verify showTemplateName was called with the template name
209
+ // The template name input is handled by the wrapper, not the InApp component
210
+ // So we verify that the callback was set up correctly
232
211
  expect(showTemplateNameMock).toBeDefined();
233
212
  });
234
213
 
235
214
  test('should preserve template name when showTemplateName callback is provided', async () => {
236
215
  const showTemplateNameMock = jest.fn();
237
-
238
216
  renderComponent({
239
- defaultData: {
240
- 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR,
241
- 'template-name': 'test',
242
- },
217
+ defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR, 'template-name': 'test' },
243
218
  showTemplateName: showTemplateNameMock,
244
219
  });
245
220
 
246
- const editor = await screen.findByTestId('inapp-html-editor');
247
- expect(editor).toBeInTheDocument();
221
+ // Wait for HTML editor to render
222
+ await waitFor(() => {
223
+ expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
224
+ });
225
+
226
+ // Verify the component renders correctly with template name
227
+ // The template name is managed by the wrapper component
248
228
  expect(showTemplateNameMock).toBeDefined();
249
229
  });
250
230
  });
@@ -252,7 +232,40 @@ describe('InApp HTMLEditor Integration', () => {
252
232
  describe('TAG API Calls', () => {
253
233
  test('should only make one TAG API call when HTML Editor is used', async () => {
254
234
  const fetchSchemaForEntityMock = jest.fn();
235
+
236
+ renderComponent({
237
+ defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR },
238
+ globalActions: {
239
+ ...defaultProps.globalActions,
240
+ fetchSchemaForEntity: fetchSchemaForEntityMock,
241
+ },
242
+ });
243
+
244
+ // Wait for component to mount and initialize
245
+ await waitFor(() => {
246
+ // Component should be rendered
247
+ expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
248
+ });
255
249
 
250
+ // Wait a bit for all useEffects to complete
251
+ await waitFor(() => {
252
+ // After isHTMLTemplate is set to true, fetchSchemaForEntity should not be called again
253
+ // The initial call might happen before isHTMLTemplate is set, but subsequent calls should not happen
254
+ // For HTML Editor, tags should only be fetched via handleOnTagsContextChange
255
+ }, { timeout: 1000 });
256
+
257
+ // For HTML Editor, fetchSchemaForEntity should not be called after isHTMLTemplate is set
258
+ // (It might be called once initially before isHTMLTemplate is set, but that's acceptable)
259
+ // The key is that it's not called multiple times
260
+ const callCount = fetchSchemaForEntityMock.mock.calls.length;
261
+ // Allow 0 or 1 calls (1 if it was called before isHTMLTemplate was set)
262
+ expect(callCount).toBeLessThanOrEqual(1);
263
+ });
264
+
265
+ test('should make TAG API call only once when handleOnTagsContextChange is called', async () => {
266
+ const fetchSchemaForEntityMock = jest.fn();
267
+
268
+ // We need to access the handler, but since it's internal, we'll test via HTMLEditor's onContextChange
256
269
  renderComponent({
257
270
  defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR },
258
271
  globalActions: {
@@ -261,8 +274,19 @@ describe('InApp HTMLEditor Integration', () => {
261
274
  },
262
275
  });
263
276
 
264
- await screen.findByTestId('inapp-html-editor');
277
+ await waitFor(() => {
278
+ expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
279
+ });
280
+
281
+ // Wait a bit for all useEffects to complete
282
+ await waitFor(() => {
283
+ // After isHTMLTemplate is set to true, fetchSchemaForEntity should not be called again
284
+ }, { timeout: 1000 });
265
285
 
286
+ // Simulate context change from HTMLEditor (which would call onContextChange)
287
+ // Since we can't directly access the handler, we verify the behavior
288
+ // by checking that fetchSchemaForEntity is not called multiple times
289
+ // (It might be called once initially before isHTMLTemplate is set, but that's acceptable)
266
290
  const callCount = fetchSchemaForEntityMock.mock.calls.length;
267
291
  expect(callCount).toBeLessThanOrEqual(1);
268
292
  });
@@ -274,37 +298,45 @@ describe('InApp HTMLEditor Integration', () => {
274
298
  defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR },
275
299
  });
276
300
 
277
- await screen.findByTestId('inapp-html-editor');
301
+ await waitFor(() => {
302
+ expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
303
+ });
278
304
 
305
+ // Simulate content being added
279
306
  const changeButton = screen.getByTestId('html-editor-change-button');
280
-
281
307
  act(() => {
282
308
  fireEvent.click(changeButton);
283
309
  });
284
310
 
311
+ // Get the layout radio buttons
285
312
  const layoutRadios = container.querySelectorAll('input[type="radio"]');
313
+ expect(layoutRadios.length).toBeGreaterThan(0);
286
314
 
315
+ // Change layout type
287
316
  if (layoutRadios.length > 1) {
288
317
  act(() => {
289
- fireEvent.change(layoutRadios[1], {
290
- target: { value: 'HEADER' },
291
- });
318
+ fireEvent.change(layoutRadios[1], { target: { value: 'HEADER' } });
292
319
  });
293
320
 
294
- const editor = await screen.findByTestId('inapp-html-editor');
295
- expect(editor).toBeInTheDocument();
321
+ // Content should still be present in the editor
322
+ // The HTMLEditor should preserve content via initialContent prop
323
+ await waitFor(() => {
324
+ const editor = screen.getByTestId('inapp-html-editor');
325
+ expect(editor).toBeInTheDocument();
326
+ });
296
327
  }
297
328
  });
298
329
  });
299
330
 
300
331
  describe('Layout Labels', () => {
301
332
  test('should use correct layout labels in constants', () => {
302
- const { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } =
303
- require('../constants');
304
-
333
+ const { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } = require('../constants');
334
+
335
+ // Verify that HEADER maps to Top banner
305
336
  expect(INAPP_MESSAGE_LAYOUT_TYPES.TOPBANNER).toBe('HEADER');
306
337
  expect(INAPP_LAYOUT_DETAILS.HEADER).toBeDefined();
307
-
338
+
339
+ // Verify that FOOTER maps to Bottom banner
308
340
  expect(INAPP_MESSAGE_LAYOUT_TYPES.BOTTOMBANNER).toBe('FOOTER');
309
341
  expect(INAPP_LAYOUT_DETAILS.FOOTER).toBeDefined();
310
342
  });
@@ -314,16 +346,29 @@ describe('InApp HTMLEditor Integration', () => {
314
346
  test('should allow creating template with Android-only content when both devices supported', async () => {
315
347
  renderComponent({
316
348
  defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR },
349
+ accountData: {
350
+ selectedWeChatAccount: {
351
+ ...defaultProps.accountData.selectedWeChatAccount,
352
+ configs: {
353
+ android: '1',
354
+ ios: '1',
355
+ },
356
+ },
357
+ },
317
358
  });
318
359
 
319
- await screen.findByTestId('inapp-html-editor');
360
+ await waitFor(() => {
361
+ expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
362
+ });
320
363
 
364
+ // Simulate adding content only for Android
321
365
  const changeButton = screen.getByTestId('html-editor-change-button');
322
-
323
366
  act(() => {
324
367
  fireEvent.click(changeButton);
325
368
  });
326
369
 
370
+ // Done button should be enabled (not disabled)
371
+ // We can't directly test isDisableDone, but we can verify the component renders
327
372
  expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
328
373
  });
329
374
  });
@@ -14,8 +14,6 @@ import CapRow from "@capillarytech/cap-ui-library/CapRow";
14
14
  import CapColumn from "@capillarytech/cap-ui-library/CapColumn";
15
15
  import CapButton from "@capillarytech/cap-ui-library/CapButton";
16
16
  import CapNotification from "@capillarytech/cap-ui-library/CapNotification";
17
- import CapTab from "@capillarytech/cap-ui-library/CapTab";
18
- import CapInput from "@capillarytech/cap-ui-library/CapInput";
19
17
  import { makeSelectInApp, makeSelectAccount, makeSelectGetTemplateDetailsInProgress } from "./selectors";
20
18
  import * as globalActions from '../Cap/actions';
21
19
  import {
@@ -54,20 +52,18 @@ import {
54
52
  IOS_CAPITAL,
55
53
  } from "./constants";
56
54
  import { INAPP, SMS } from "../CreativesContainer/constants";
57
- import { AI_CONTENT_BOT_DISABLED } from "../../constants/unified";
58
55
  import {
59
56
  ALL, TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
60
57
  } from "../Whatsapp/constants";
61
58
  import { getCdnUrl } from "../../utils/cdnTransformation";
62
59
  import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
63
60
  import { validateInAppContent } from "../../utils/commonUtils";
64
- import { hasLiquidSupportFeature, hasNewEditorFlowInAppEnabled } from "../../utils/common";
61
+ import { hasLiquidSupportFeature } from "../../utils/common";
65
62
  import formBuilderMessages from "../../v2Components/FormBuilder/messages";
66
63
  import HTMLEditor from "../../v2Components/HtmlEditor";
67
64
  import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
68
65
  import { INAPP_EDITOR_TYPES } from "../InAppWrapper/constants";
69
66
  import InappAdvanced from "../InappAdvance/index";
70
- import CapDeviceContent from "../../v2Components/CapDeviceContent";
71
67
  import { ErrorInfoNote } from "../../v2Components/ErrorInfoNote";
72
68
 
73
69
  let editContent = {};
@@ -78,7 +74,6 @@ export const InApp = (props) => {
78
74
  actions,
79
75
  globalActions,
80
76
  isFullMode,
81
- isLoyaltyModule,
82
77
  onCreateComplete,
83
78
  params,
84
79
  templateData = {},
@@ -883,7 +878,7 @@ export const InApp = (props) => {
883
878
  // Use 'html editor template' as title for HTML editor to differentiate from BEE editor
884
879
  title: isHTMLTemplate ? 'html editor template' : titleAndroid,
885
880
  message: androidMessage,
886
- bodyType: bodyTypeForBackend,
881
+ bodyType: templateLayoutType,
887
882
  expandableDetails: {
888
883
  style: androidExpandableStyle,
889
884
  message: androidMessage,
@@ -1245,13 +1240,11 @@ export const InApp = (props) => {
1245
1240
  && !isBEEeditor
1246
1241
  && !isBeeFreeTemplate;
1247
1242
 
1248
- const isNewEditorFlowEnabled = !isLoyaltyModule && hasNewEditorFlowInAppEnabled();
1249
-
1250
1243
  // Use state if available, otherwise fall back to direct data check
1251
- const shouldUseHTMLEditor = isNewEditorFlowEnabled && (isHTMLTemplate || isHTMLTemplateFromData);
1244
+ const shouldUseHTMLEditor = isHTMLTemplate || isHTMLTemplateFromData;
1252
1245
 
1253
1246
  // Only route to Bee editor if it's explicitly a Bee editor AND not an HTML template
1254
- const shouldUseBeeEditor = isNewEditorFlowEnabled && (isBEEeditor || isBeeFreeTemplate) && !shouldUseHTMLEditor;
1247
+ const shouldUseBeeEditor = (isBEEeditor || isBeeFreeTemplate) && !shouldUseHTMLEditor;
1255
1248
 
1256
1249
  // Early returns to avoid nested ternary
1257
1250
  if (isEditInApp && getTemplateDetailsInProgress) {
@@ -1287,157 +1280,10 @@ export const InApp = (props) => {
1287
1280
  );
1288
1281
  }
1289
1282
 
1290
- // ── Old-flow helpers (used by CapDeviceContent when flag is disabled) ──────
1291
- const isAiContentBotDisabled = currentOrgDetails?.accessibleFeatures?.includes(AI_CONTENT_BOT_DISABLED);
1292
-
1293
- const templateDescErrorHandler = (value) => {
1294
- const { unsupportedTags, isBraceError } = validateTags({
1295
- content: value,
1296
- tagsParam: tags,
1297
- injectedTagsParams: injectedTags,
1298
- location,
1299
- tagModule: getDefaultTags,
1300
- isFullMode,
1301
- }) || {};
1302
- if (unsupportedTags?.length > 0) {
1303
- return formatMessage(globalMessages.unsupportedTagsValidationError, { unsupportedTags });
1304
- }
1305
- if (isBraceError) {
1306
- return formatMessage(globalMessages.braceValidationError);
1307
- }
1308
- return '';
1309
- };
1310
-
1311
- const onCopyTitleAndContent = () => {
1312
- if (panes === ANDROID) {
1313
- setTitleAndroid(titleIos);
1314
- setTemplateMessageAndroid(templateMessageIos);
1315
- setInAppImageSrcAndroid(inAppImageSrcIos);
1316
- } else {
1317
- setTitleIos(titleAndroid);
1318
- setTemplateMessageIos(templateMessageAndroid);
1319
- setInAppImageSrcIos(inAppImageSrcAndroid);
1320
- }
1321
- };
1322
-
1323
- const onTagSelect = (value, index) => {
1324
- const tag = `{{${value}}}`;
1325
- if (panes === ANDROID) {
1326
- if (index === 0) setTitleAndroid((prev) => prev + tag);
1327
- else setTemplateMessageAndroid((prev) => prev + tag);
1328
- } else if (index === 0) {
1329
- setTitleIos((prev) => prev + tag);
1330
- } else {
1331
- setTemplateMessageIos((prev) => prev + tag);
1332
- }
1333
- };
1334
-
1335
- // Device support flags (same derivation as InappAdvance)
1336
- const isAndroidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
1337
- const isIosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
1338
-
1339
- // CapDeviceContent tab panes (old flow)
1340
- const DEVICE_PANES = [
1341
- {
1342
- content: (
1343
- <CapDeviceContent
1344
- panes={ANDROID}
1345
- actions={actions}
1346
- editData={editData}
1347
- isFullMode={isFullMode}
1348
- inAppImageSrc={inAppImageSrcAndroid}
1349
- setInAppImageSrc={setInAppImageSrcAndroid}
1350
- isEditFlow={isEditFlow}
1351
- ctaData={ctaDataAndroid}
1352
- setCtaData={setCtaDataAndroid}
1353
- buttonType={buttonTypeAndroid}
1354
- setButtonType={setButtonTypeAndroid}
1355
- templateMediaType={templateMediaType}
1356
- setTemplateMediaType={setTemplateMediaType}
1357
- title={titleAndroid}
1358
- setTitle={setTitleAndroid}
1359
- templateMessageError={templateMessageErrorAndroid}
1360
- templateMessage={templateMessageAndroid}
1361
- setTemplateMessage={setTemplateMessageAndroid}
1362
- setTemplateMessageError={setTemplateMessageErrorAndroid}
1363
- addActionLink={addActionLinkAndroid}
1364
- setAddActionLink={setAddActionLinkAndroid}
1365
- deepLink={deepLink}
1366
- deepLinkValue={deepLinkValueAndroid}
1367
- setDeepLinkValue={setDeepLinkValueAndroid}
1368
- onCopyTitleAndContent={onCopyTitleAndContent}
1369
- isOtherDeviceSupported={isIosSupported}
1370
- tags={tags}
1371
- onTagSelect={onTagSelect}
1372
- handleOnTagsContextChange={handleOnTagsContextChange}
1373
- templateDescErrorHandler={templateDescErrorHandler}
1374
- templateTitleError={templateTitleErrorAndroid}
1375
- setTemplateTitleError={setTemplateTitleErrorAndroid}
1376
- isAiContentBotDisabled={isAiContentBotDisabled}
1377
- injectedTags={injectedTags}
1378
- selectedOfferDetails={selectedOfferDetails}
1379
- location={location}
1380
- />
1381
- ),
1382
- tab: <FormattedMessage {...messages.Android} />,
1383
- key: ANDROID,
1384
- isSupported: isAndroidSupported,
1385
- },
1386
- {
1387
- content: (
1388
- <CapDeviceContent
1389
- panes={IOS}
1390
- actions={actions}
1391
- editData={editData}
1392
- isFullMode={isFullMode}
1393
- inAppImageSrc={inAppImageSrcIos}
1394
- setInAppImageSrc={setInAppImageSrcIos}
1395
- isEditFlow={isEditFlow}
1396
- ctaData={ctaDataIos}
1397
- setCtaData={setCtaDataIos}
1398
- buttonType={buttonTypeIos}
1399
- setButtonType={setButtonTypeIos}
1400
- templateMediaType={templateMediaType}
1401
- setTemplateMediaType={setTemplateMediaType}
1402
- title={titleIos}
1403
- setTitle={setTitleIos}
1404
- templateMessageError={templateMessageErrorIos}
1405
- templateMessage={templateMessageIos}
1406
- setTemplateMessage={setTemplateMessageIos}
1407
- setTemplateMessageError={setTemplateMessageErrorIos}
1408
- addActionLink={addActionLinkIos}
1409
- setAddActionLink={setAddActionLinkIos}
1410
- deepLink={deepLink}
1411
- deepLinkValue={deepLinkValueIos}
1412
- setDeepLinkValue={setDeepLinkValueIos}
1413
- onCopyTitleAndContent={onCopyTitleAndContent}
1414
- isOtherDeviceSupported={isAndroidSupported}
1415
- tags={tags}
1416
- onTagSelect={onTagSelect}
1417
- handleOnTagsContextChange={handleOnTagsContextChange}
1418
- templateDescErrorHandler={templateDescErrorHandler}
1419
- templateTitleError={templateTitleErrorIos}
1420
- setTemplateTitleError={setTemplateTitleErrorIos}
1421
- isAiContentBotDisabled={isAiContentBotDisabled}
1422
- injectedTags={injectedTags}
1423
- selectedOfferDetails={selectedOfferDetails}
1424
- location={location}
1425
- />
1426
- ),
1427
- tab: <FormattedMessage {...messages.Ios} />,
1428
- key: IOS,
1429
- isSupported: isIosSupported,
1430
- },
1431
- ];
1432
- // ─────────────────────────────────────────────────────────────────────────
1433
-
1434
- // Calculate column span: HTML editor = 18, everything else = 24
1435
- const editorColumnSpan = shouldUseHTMLEditor ? 18 : 24;
1436
-
1437
1283
  return (
1438
1284
  <CapSpin spinning={spin || fetchingLiquidValidation} tip={fetchingLiquidValidation ? <FormattedMessage {...formBuilderMessages.liquidSpinText} /> : ""}>
1439
1285
  <CapRow className="cap-inapp-creatives">
1440
- <CapColumn span={editorColumnSpan}>
1286
+ <CapColumn span={shouldUseHTMLEditor ? 18 : 24}>
1441
1287
  {/* Creative layout type */}
1442
1288
  {shouldUseHTMLEditor && (
1443
1289
  <>
@@ -1458,7 +1304,7 @@ export const InApp = (props) => {
1458
1304
  />
1459
1305
  </>
1460
1306
  )}
1461
- {shouldUseHTMLEditor && (
1307
+ {shouldUseHTMLEditor ? (
1462
1308
  <HTMLEditor
1463
1309
  key={`inapp-html-editor-v${htmlEditorContentVersion}`}
1464
1310
  variant={HTML_EDITOR_VARIANTS.INAPP}
@@ -1474,16 +1320,19 @@ export const InApp = (props) => {
1474
1320
  location={location}
1475
1321
  selectedOfferDetails={selectedOfferDetails}
1476
1322
  onTagSelect={() => {
1477
- // Tag insertion handled by HTMLEditor's CodeEditorPane at cursor position
1323
+ // Tag insertion is handled by HTMLEditor's CodeEditorPane at cursor position
1324
+ // Content updates will be propagated via onContentChange callback
1478
1325
  }}
1326
+ // Don't pass globalActions to prevent duplicate API calls
1327
+ // HTMLEditor will use onContextChange instead
1479
1328
  onContextChange={handleOnTagsContextChange}
1329
+ // Pass validation errors to HTMLEditor for display
1480
1330
  errors={errorMessage}
1481
1331
  isFullMode={isFullMode}
1482
1332
  data-test="inapp-html-editor"
1483
1333
  style={{ width: '138%' }}
1484
1334
  />
1485
- )}
1486
- {!shouldUseHTMLEditor && isNewEditorFlowEnabled && (
1335
+ ) : (
1487
1336
  <InappAdvanced
1488
1337
  getFormData={getFormData}
1489
1338
  setIsLoadingContent={setIsLoadingContent}
@@ -1508,29 +1357,11 @@ export const InApp = (props) => {
1508
1357
  onCreateComplete={onCreateComplete}
1509
1358
  />
1510
1359
  )}
1511
- {!shouldUseHTMLEditor && !isNewEditorFlowEnabled && (
1512
- <>
1513
- <CapInput
1514
- label={<FormattedMessage {...messages.creativeName} />}
1515
- onChange={({ target: { value } }) => setTempName(value)}
1516
- value={tempName}
1517
- labelPosition="top"
1518
- size="default"
1519
- />
1520
- <CapTab
1521
- panes={DEVICE_PANES.filter((devicePane) => devicePane?.isSupported === true)}
1522
- onChange={(value) => setPanes(value)}
1523
- activeKey={panes}
1524
- defaultActiveKey={panes}
1525
- className="inapp-template-device-tab"
1526
- />
1527
- </>
1528
- )}
1529
1360
  </CapColumn>
1530
1361
  </CapRow>
1531
- {/* Footer with Done/Update button - show for HTML editor and old CapDeviceContent flow */}
1362
+ {/* Footer with Done/Update button - Only show for HTML editor, bee editor has its own footer */}
1532
1363
  {
1533
- (shouldUseHTMLEditor || !isNewEditorFlowEnabled) && (
1364
+ shouldUseHTMLEditor && (
1534
1365
  <>
1535
1366
  {hasAnyErrors(errorMessage) && (
1536
1367
  <ErrorInfoNote
@@ -894,5 +894,5 @@ export const deviceContentProps = {
894
894
  },
895
895
  ],
896
896
  tags: [],
897
- isOtherDeviceSupported: true,
898
897
  };
898
+