@capillarytech/creatives-library 8.0.301 → 8.0.302

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,6 +45,7 @@ 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';
48
49
  export const SUPPORT_CK_EDITOR = 'SUPPORT_CK_EDITOR';
49
50
  export const CUSTOM_TAG = 'CustomTagMessage';
50
51
  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.301",
4
+ "version": "8.0.302",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -24,7 +24,8 @@ import {
24
24
  ENABLE_WEBPUSH,
25
25
  LIQUID_SUPPORT,
26
26
  SUPPORT_CK_EDITOR,
27
- ENABLE_NEW_MPUSH
27
+ ENABLE_NEW_MPUSH,
28
+ ENABLE_NEW_EDITOR_FLOW_INAPP
28
29
  } from '../constants/unified';
29
30
  import { apiMessageFormatHandler } from './commonUtils';
30
31
 
@@ -142,6 +143,11 @@ export const hasNewMobilePushFeatureEnabled = Auth.hasFeatureAccess.bind(
142
143
  ENABLE_NEW_MPUSH,
143
144
  );
144
145
 
146
+ export const hasNewEditorFlowInAppEnabled = Auth.hasFeatureAccess.bind(
147
+ null,
148
+ ENABLE_NEW_EDITOR_FLOW_INAPP,
149
+ );
150
+
145
151
  //filtering tags based on scope
146
152
  export const filterTags = (tagsToFilter, tagsList) => tagsList?.filter(
147
153
  (tag) => !tagsToFilter?.includes(tag?.definition?.value)
@@ -63,6 +63,7 @@ const CapDeviceContent = (props) => {
63
63
  deepLinkValue,
64
64
  setDeepLinkValue,
65
65
  onCopyTitleAndContent,
66
+ isOtherDeviceSupported,
66
67
  tags,
67
68
  onTagSelect,
68
69
  handleOnTagsContextChange,
@@ -167,13 +168,15 @@ const CapDeviceContent = (props) => {
167
168
  return (
168
169
  <>
169
170
  <CapRow className="creatives-device-content">
170
- <CapLink
171
- title={isAndroid
172
- ? formatMessage(messages.copyContentFromIOS)
173
- : formatMessage(messages.copyCotentFromAndroid)}
174
- className="inapp-copy-content"
175
- onClick={onCopyTitleAndContent}
176
- />
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
+ )}
177
180
  <CapRow className="creatives-inapp-title">
178
181
  <CapColumn
179
182
  className="inapp-content-main"
@@ -40,6 +40,11 @@ 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]);
43
48
 
44
49
  const [visibleTaglist, setVisibleTaglist] = useState(false);
45
50
  const [selectedTag, setSelectedTag] = useState({});
@@ -111,8 +116,10 @@ function BeePopupEditor(props) {
111
116
  window.BeePlugin.create(tokenData, beeConfig, (instance) => {
112
117
  beePluginInstance = instance;
113
118
  beeInstanceRef.current = instance;
114
- // Check if beeJson is already an object (happens when layout type changes)
115
- const parseJson = typeof beeJson === 'string' ? JSON.parse(beeJson) : beeJson;
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;
116
123
  beePluginInstance.start(parseJson);
117
124
  saveBeeInstance(beePluginInstance, device);
118
125
  isInitializedRef.current = true;
@@ -1078,8 +1078,38 @@ export function SlideBoxContent(props) {
1078
1078
  )}
1079
1079
 
1080
1080
  {isCreateInApp && (
1081
- <InAppWrapper
1082
- key="creatives-inapp-wrapper"
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"
1083
1113
  date={new Date().getMilliseconds()}
1084
1114
  setIsLoadingContent={setIsLoadingContent}
1085
1115
  onInAppEditorTypeChange={onInAppEditorTypeChange}
@@ -1114,10 +1144,12 @@ export function SlideBoxContent(props) {
1114
1144
  handleCloseTestAndPreview={handleCloseTestAndPreview}
1115
1145
  isTestAndPreviewMode={isTestAndPreviewMode}
1116
1146
  />
1147
+ )
1117
1148
  )}
1118
-
1149
+
1119
1150
  {isEditInApp && (<InApp
1120
1151
  isFullMode={isFullMode}
1152
+ isLoyaltyModule={isLoyaltyModule}
1121
1153
  templateData={templateData}
1122
1154
  getFormData={getFormData}
1123
1155
  getDefaultTags={type}
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
3
-
4
3
  import { SlideBoxContent } from '../SlideBoxContent';
5
4
  import mockdata from '../../mockdata';
6
5
  import { templateDetailsImage, templateDetailsVideo, templateDetailsText, templateDetails_ } from '../../Viber/tests/mockData';
@@ -42,6 +41,15 @@ jest.mock('../../WebPush', () => ({
42
41
  ),
43
42
  }));
44
43
 
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
+
45
53
  describe('Test SlideBoxContent container', () => {
46
54
  const onCreateComplete = jest.fn();
47
55
  let renderedComponent;
@@ -910,4 +918,64 @@ describe('Test SlideBoxContent container', () => {
910
918
  expect(renderedComponent).toMatchSnapshot();
911
919
  });
912
920
  });
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
+ });
913
981
  });
@@ -202,6 +202,114 @@ 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
+
205
313
  exports[`Test SlideBoxContent container Should handle isTestAndPreviewMode IIFE implementation correctly 1`] = `
206
314
  <SlideBoxContent__CreativesWrapper>
207
315
  <ForwardRef
@@ -1103,17 +1211,26 @@ exports[`Test SlideBoxContent container Should render correct component for what
1103
1211
 
1104
1212
  exports[`Test SlideBoxContent container Should render correct component for whatsapp channel create mode 2`] = `
1105
1213
  <SlideBoxContent__CreativesWrapper>
1106
- <Connect(Connect(UserIsAuthenticated(InjectIntl(CreativesCommon))))
1107
- date={0}
1214
+ <Component
1108
1215
  getDefaultTags=""
1109
- key="creatives-inapp-wrapper"
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
+ }
1110
1228
  onCreateComplete={[MockFunction]}
1111
1229
  templateData={
1112
1230
  Object {
1113
1231
  "mode": "create",
1114
1232
  }
1115
1233
  }
1116
- type=""
1117
1234
  />
1118
1235
  </SlideBoxContent__CreativesWrapper>
1119
1236
  `;
@@ -39,33 +39,58 @@ 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
- const MockHTMLEditor = function MockHTMLEditor({ variant, initialContent, onContentChange, onSave, 'data-test': dataTest }) {
43
- return React.createElement('div', { 'data-testid': dataTest || 'html-editor' },
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' },
44
53
  React.createElement('div', { 'data-testid': 'html-editor-variant' }, variant),
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')
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
+ )
69
94
  );
70
95
  };
71
96
  MockHTMLEditor.displayName = 'MockHTMLEditor';
@@ -75,22 +100,25 @@ jest.mock('../../../v2Components/HtmlEditor', () => {
75
100
  };
76
101
  });
77
102
 
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';
88
-
89
-
90
- // Mock other dependencies
103
+ /**
104
+ * Mock CapDeviceContent to ensure HTMLEditor renders
105
+ */
91
106
  jest.mock('../../../v2Components/CapDeviceContent', () => {
107
+ const React = require('react');
108
+ const HTMLEditor = require('../../../v2Components/HtmlEditor').default;
109
+
92
110
  return function MockCapDeviceContent() {
93
- return <div data-testid="cap-device-content">Device Content</div>;
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
+ );
94
122
  };
95
123
  });
96
124
 
@@ -100,7 +128,20 @@ jest.mock('../../../v2Components/TemplatePreview', () => {
100
128
  };
101
129
  });
102
130
 
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
+
103
143
  let store;
144
+
104
145
  beforeAll(() => {
105
146
  store = configureStore({}, initialReducer, history);
106
147
  });
@@ -135,8 +176,6 @@ const defaultProps = {
135
176
  configs: {
136
177
  android: '1',
137
178
  ios: '1',
138
- accessToken: 'test-token',
139
- deeplink: '{}',
140
179
  },
141
180
  },
142
181
  },
@@ -145,7 +184,6 @@ const defaultProps = {
145
184
  query: {},
146
185
  search: '',
147
186
  },
148
- getDefaultTags: null,
149
187
  supportedTags: [],
150
188
  metaEntities: {
151
189
  tags: {
@@ -158,26 +196,11 @@ const defaultProps = {
158
196
  currentOrgDetails: {
159
197
  accessibleFeatures: [],
160
198
  },
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 };
170
199
  };
171
200
 
172
201
  const renderComponent = (props = {}) => {
173
202
  const mergedProps = { ...defaultProps, ...props };
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
- }
203
+
181
204
  return render(
182
205
  <Provider store={store}>
183
206
  <IntlProvider locale="en" messages={{}}>
@@ -195,36 +218,33 @@ describe('InApp HTMLEditor Integration', () => {
195
218
  describe('Template Name Editing', () => {
196
219
  test('should not truncate template name when editing', async () => {
197
220
  const showTemplateNameMock = jest.fn();
221
+
198
222
  renderComponent({
199
- defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR, 'template-name': 'abcd' },
223
+ defaultData: {
224
+ 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR,
225
+ 'template-name': 'abcd',
226
+ },
200
227
  showTemplateName: showTemplateNameMock,
201
228
  });
202
229
 
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
230
+ const editor = await screen.findByTestId('inapp-html-editor');
231
+ expect(editor).toBeInTheDocument();
211
232
  expect(showTemplateNameMock).toBeDefined();
212
233
  });
213
234
 
214
235
  test('should preserve template name when showTemplateName callback is provided', async () => {
215
236
  const showTemplateNameMock = jest.fn();
237
+
216
238
  renderComponent({
217
- defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR, 'template-name': 'test' },
239
+ defaultData: {
240
+ 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR,
241
+ 'template-name': 'test',
242
+ },
218
243
  showTemplateName: showTemplateNameMock,
219
244
  });
220
245
 
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
246
+ const editor = await screen.findByTestId('inapp-html-editor');
247
+ expect(editor).toBeInTheDocument();
228
248
  expect(showTemplateNameMock).toBeDefined();
229
249
  });
230
250
  });
@@ -232,40 +252,7 @@ describe('InApp HTMLEditor Integration', () => {
232
252
  describe('TAG API Calls', () => {
233
253
  test('should only make one TAG API call when HTML Editor is used', async () => {
234
254
  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
- });
249
255
 
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
269
256
  renderComponent({
270
257
  defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR },
271
258
  globalActions: {
@@ -274,19 +261,8 @@ describe('InApp HTMLEditor Integration', () => {
274
261
  },
275
262
  });
276
263
 
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 });
264
+ await screen.findByTestId('inapp-html-editor');
285
265
 
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)
290
266
  const callCount = fetchSchemaForEntityMock.mock.calls.length;
291
267
  expect(callCount).toBeLessThanOrEqual(1);
292
268
  });
@@ -298,45 +274,37 @@ describe('InApp HTMLEditor Integration', () => {
298
274
  defaultData: { 'editor-type': INAPP_EDITOR_TYPES.HTML_EDITOR },
299
275
  });
300
276
 
301
- await waitFor(() => {
302
- expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
303
- });
277
+ await screen.findByTestId('inapp-html-editor');
304
278
 
305
- // Simulate content being added
306
279
  const changeButton = screen.getByTestId('html-editor-change-button');
280
+
307
281
  act(() => {
308
282
  fireEvent.click(changeButton);
309
283
  });
310
284
 
311
- // Get the layout radio buttons
312
285
  const layoutRadios = container.querySelectorAll('input[type="radio"]');
313
- expect(layoutRadios.length).toBeGreaterThan(0);
314
286
 
315
- // Change layout type
316
287
  if (layoutRadios.length > 1) {
317
288
  act(() => {
318
- fireEvent.change(layoutRadios[1], { target: { value: 'HEADER' } });
289
+ fireEvent.change(layoutRadios[1], {
290
+ target: { value: 'HEADER' },
291
+ });
319
292
  });
320
293
 
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
- });
294
+ const editor = await screen.findByTestId('inapp-html-editor');
295
+ expect(editor).toBeInTheDocument();
327
296
  }
328
297
  });
329
298
  });
330
299
 
331
300
  describe('Layout Labels', () => {
332
301
  test('should use correct layout labels in constants', () => {
333
- const { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } = require('../constants');
334
-
335
- // Verify that HEADER maps to Top banner
302
+ const { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } =
303
+ require('../constants');
304
+
336
305
  expect(INAPP_MESSAGE_LAYOUT_TYPES.TOPBANNER).toBe('HEADER');
337
306
  expect(INAPP_LAYOUT_DETAILS.HEADER).toBeDefined();
338
-
339
- // Verify that FOOTER maps to Bottom banner
307
+
340
308
  expect(INAPP_MESSAGE_LAYOUT_TYPES.BOTTOMBANNER).toBe('FOOTER');
341
309
  expect(INAPP_LAYOUT_DETAILS.FOOTER).toBeDefined();
342
310
  });
@@ -346,29 +314,16 @@ describe('InApp HTMLEditor Integration', () => {
346
314
  test('should allow creating template with Android-only content when both devices supported', async () => {
347
315
  renderComponent({
348
316
  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
- },
358
317
  });
359
318
 
360
- await waitFor(() => {
361
- expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
362
- });
319
+ await screen.findByTestId('inapp-html-editor');
363
320
 
364
- // Simulate adding content only for Android
365
321
  const changeButton = screen.getByTestId('html-editor-change-button');
322
+
366
323
  act(() => {
367
324
  fireEvent.click(changeButton);
368
325
  });
369
326
 
370
- // Done button should be enabled (not disabled)
371
- // We can't directly test isDisableDone, but we can verify the component renders
372
327
  expect(screen.getByTestId('inapp-html-editor')).toBeInTheDocument();
373
328
  });
374
329
  });
@@ -14,6 +14,8 @@ 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";
17
19
  import { makeSelectInApp, makeSelectAccount, makeSelectGetTemplateDetailsInProgress } from "./selectors";
18
20
  import * as globalActions from '../Cap/actions';
19
21
  import {
@@ -52,18 +54,20 @@ import {
52
54
  IOS_CAPITAL,
53
55
  } from "./constants";
54
56
  import { INAPP, SMS } from "../CreativesContainer/constants";
57
+ import { AI_CONTENT_BOT_DISABLED } from "../../constants/unified";
55
58
  import {
56
59
  ALL, TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
57
60
  } from "../Whatsapp/constants";
58
61
  import { getCdnUrl } from "../../utils/cdnTransformation";
59
62
  import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
60
63
  import { validateInAppContent } from "../../utils/commonUtils";
61
- import { hasLiquidSupportFeature } from "../../utils/common";
64
+ import { hasLiquidSupportFeature, hasNewEditorFlowInAppEnabled } from "../../utils/common";
62
65
  import formBuilderMessages from "../../v2Components/FormBuilder/messages";
63
66
  import HTMLEditor from "../../v2Components/HtmlEditor";
64
67
  import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
65
68
  import { INAPP_EDITOR_TYPES } from "../InAppWrapper/constants";
66
69
  import InappAdvanced from "../InappAdvance/index";
70
+ import CapDeviceContent from "../../v2Components/CapDeviceContent";
67
71
  import { ErrorInfoNote } from "../../v2Components/ErrorInfoNote";
68
72
 
69
73
  let editContent = {};
@@ -74,6 +78,7 @@ export const InApp = (props) => {
74
78
  actions,
75
79
  globalActions,
76
80
  isFullMode,
81
+ isLoyaltyModule,
77
82
  onCreateComplete,
78
83
  params,
79
84
  templateData = {},
@@ -878,7 +883,7 @@ export const InApp = (props) => {
878
883
  // Use 'html editor template' as title for HTML editor to differentiate from BEE editor
879
884
  title: isHTMLTemplate ? 'html editor template' : titleAndroid,
880
885
  message: androidMessage,
881
- bodyType: templateLayoutType,
886
+ bodyType: bodyTypeForBackend,
882
887
  expandableDetails: {
883
888
  style: androidExpandableStyle,
884
889
  message: androidMessage,
@@ -1240,11 +1245,13 @@ export const InApp = (props) => {
1240
1245
  && !isBEEeditor
1241
1246
  && !isBeeFreeTemplate;
1242
1247
 
1248
+ const isNewEditorFlowEnabled = !isLoyaltyModule && hasNewEditorFlowInAppEnabled();
1249
+
1243
1250
  // Use state if available, otherwise fall back to direct data check
1244
- const shouldUseHTMLEditor = isHTMLTemplate || isHTMLTemplateFromData;
1251
+ const shouldUseHTMLEditor = isNewEditorFlowEnabled && (isHTMLTemplate || isHTMLTemplateFromData);
1245
1252
 
1246
1253
  // Only route to Bee editor if it's explicitly a Bee editor AND not an HTML template
1247
- const shouldUseBeeEditor = (isBEEeditor || isBeeFreeTemplate) && !shouldUseHTMLEditor;
1254
+ const shouldUseBeeEditor = isNewEditorFlowEnabled && (isBEEeditor || isBeeFreeTemplate) && !shouldUseHTMLEditor;
1248
1255
 
1249
1256
  // Early returns to avoid nested ternary
1250
1257
  if (isEditInApp && getTemplateDetailsInProgress) {
@@ -1280,10 +1287,157 @@ export const InApp = (props) => {
1280
1287
  );
1281
1288
  }
1282
1289
 
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
+
1283
1437
  return (
1284
1438
  <CapSpin spinning={spin || fetchingLiquidValidation} tip={fetchingLiquidValidation ? <FormattedMessage {...formBuilderMessages.liquidSpinText} /> : ""}>
1285
1439
  <CapRow className="cap-inapp-creatives">
1286
- <CapColumn span={shouldUseHTMLEditor ? 18 : 24}>
1440
+ <CapColumn span={editorColumnSpan}>
1287
1441
  {/* Creative layout type */}
1288
1442
  {shouldUseHTMLEditor && (
1289
1443
  <>
@@ -1304,7 +1458,7 @@ export const InApp = (props) => {
1304
1458
  />
1305
1459
  </>
1306
1460
  )}
1307
- {shouldUseHTMLEditor ? (
1461
+ {shouldUseHTMLEditor && (
1308
1462
  <HTMLEditor
1309
1463
  key={`inapp-html-editor-v${htmlEditorContentVersion}`}
1310
1464
  variant={HTML_EDITOR_VARIANTS.INAPP}
@@ -1320,19 +1474,16 @@ export const InApp = (props) => {
1320
1474
  location={location}
1321
1475
  selectedOfferDetails={selectedOfferDetails}
1322
1476
  onTagSelect={() => {
1323
- // Tag insertion is handled by HTMLEditor's CodeEditorPane at cursor position
1324
- // Content updates will be propagated via onContentChange callback
1477
+ // Tag insertion handled by HTMLEditor's CodeEditorPane at cursor position
1325
1478
  }}
1326
- // Don't pass globalActions to prevent duplicate API calls
1327
- // HTMLEditor will use onContextChange instead
1328
1479
  onContextChange={handleOnTagsContextChange}
1329
- // Pass validation errors to HTMLEditor for display
1330
1480
  errors={errorMessage}
1331
1481
  isFullMode={isFullMode}
1332
1482
  data-test="inapp-html-editor"
1333
1483
  style={{ width: '138%' }}
1334
1484
  />
1335
- ) : (
1485
+ )}
1486
+ {!shouldUseHTMLEditor && isNewEditorFlowEnabled && (
1336
1487
  <InappAdvanced
1337
1488
  getFormData={getFormData}
1338
1489
  setIsLoadingContent={setIsLoadingContent}
@@ -1357,11 +1508,29 @@ export const InApp = (props) => {
1357
1508
  onCreateComplete={onCreateComplete}
1358
1509
  />
1359
1510
  )}
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
+ )}
1360
1529
  </CapColumn>
1361
1530
  </CapRow>
1362
- {/* Footer with Done/Update button - Only show for HTML editor, bee editor has its own footer */}
1531
+ {/* Footer with Done/Update button - show for HTML editor and old CapDeviceContent flow */}
1363
1532
  {
1364
- shouldUseHTMLEditor && (
1533
+ (shouldUseHTMLEditor || !isNewEditorFlowEnabled) && (
1365
1534
  <>
1366
1535
  {hasAnyErrors(errorMessage) && (
1367
1536
  <ErrorInfoNote
@@ -894,5 +894,5 @@ export const deviceContentProps = {
894
894
  },
895
895
  ],
896
896
  tags: [],
897
+ isOtherDeviceSupported: true,
897
898
  };
898
-