@capillarytech/creatives-library 8.0.287-alpha.3 → 8.0.288

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/constants/unified.js +1 -0
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +111 -2
  6. package/utils/tagValidations.js +222 -84
  7. package/utils/tests/commonUtil.test.js +118 -147
  8. package/utils/tests/tagValidations.test.js +358 -280
  9. package/v2Components/CapTagList/index.js +7 -2
  10. package/v2Components/CapTagListWithInput/index.js +4 -0
  11. package/v2Components/ErrorInfoNote/index.js +5 -2
  12. package/v2Components/FormBuilder/index.js +187 -74
  13. package/v2Components/FormBuilder/messages.js +12 -0
  14. package/v2Components/HtmlEditor/HTMLEditor.js +5 -0
  15. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  16. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  17. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +2 -1
  18. package/v2Containers/Cap/mockData.js +14 -0
  19. package/v2Containers/Cap/reducer.js +55 -3
  20. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  21. package/v2Containers/CreativesContainer/SlideBoxContent.js +20 -0
  22. package/v2Containers/CreativesContainer/SlideBoxFooter.js +40 -6
  23. package/v2Containers/CreativesContainer/constants.js +6 -0
  24. package/v2Containers/CreativesContainer/index.js +36 -5
  25. package/v2Containers/CreativesContainer/messages.js +12 -0
  26. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +339 -0
  27. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +18 -0
  28. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +37 -0
  29. package/v2Containers/Email/index.js +5 -1
  30. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +62 -10
  31. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +115 -12
  32. package/v2Containers/FTP/index.js +51 -2
  33. package/v2Containers/FTP/messages.js +4 -0
  34. package/v2Containers/InApp/index.js +96 -1
  35. package/v2Containers/InApp/tests/index.test.js +6 -17
  36. package/v2Containers/InappAdvance/index.js +103 -2
  37. package/v2Containers/Line/Container/Text/index.js +1 -0
  38. package/v2Containers/MobilePush/Create/index.js +37 -1
  39. package/v2Containers/MobilePush/Create/messages.js +4 -0
  40. package/v2Containers/MobilePush/Edit/index.js +37 -2
  41. package/v2Containers/MobilePush/Edit/messages.js +4 -0
  42. package/v2Containers/MobilePushNew/components/PlatformContentFields.js +36 -12
  43. package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +68 -27
  44. package/v2Containers/MobilePushNew/index.js +92 -5
  45. package/v2Containers/MobilePushNew/messages.js +8 -0
  46. package/v2Containers/MobilepushWrapper/index.js +7 -1
  47. package/v2Containers/Rcs/index.js +37 -12
  48. package/v2Containers/Sms/Create/index.js +3 -31
  49. package/v2Containers/Sms/Create/messages.js +0 -4
  50. package/v2Containers/Sms/Edit/index.js +3 -29
  51. package/v2Containers/Sms/commonMethods.js +6 -6
  52. package/v2Containers/SmsTrai/Edit/index.js +47 -6
  53. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  54. package/v2Containers/TagList/index.js +17 -1
  55. package/v2Containers/TagList/messages.js +4 -0
  56. package/v2Containers/TemplatesV2/index.js +43 -23
  57. package/v2Containers/Viber/index.js +1 -0
  58. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  59. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  60. package/v2Containers/WebPush/Create/index.js +25 -6
  61. package/v2Containers/WebPush/Create/messages.js +8 -1
  62. package/v2Containers/WebPush/Create/utils/validation.js +20 -22
  63. package/v2Containers/WebPush/Create/utils/validation.test.js +52 -0
  64. package/v2Containers/Whatsapp/index.js +17 -9
  65. package/v2Containers/Zalo/index.js +11 -3
@@ -13,7 +13,7 @@ import { IntlProvider } from 'react-intl';
13
13
  import EmailHTMLEditor from '../EmailHTMLEditor';
14
14
  import { validateLiquidTemplateContent } from '../../../../utils/commonUtils';
15
15
  import { validateTags } from '../../../../utils/tagValidations';
16
- import { isEmailUnsubscribeTagOptional } from '../../../../utils/common';
16
+ import { isEmailUnsubscribeTagMandatory } from '../../../../utils/common';
17
17
 
18
18
  // Mock dependencies
19
19
  jest.mock('../../../../utils/commonUtils', () => ({
@@ -24,8 +24,11 @@ jest.mock('../../../../utils/tagValidations', () => ({
24
24
  validateTags: jest.fn(),
25
25
  }));
26
26
 
27
+ // Create mutable mock for hasLiquidSupportFeature
28
+ const mockHasLiquidSupportFeature = jest.fn(() => true);
27
29
  jest.mock('../../../../utils/common', () => ({
28
- isEmailUnsubscribeTagOptional: jest.fn(() => false),
30
+ hasLiquidSupportFeature: (...args) => mockHasLiquidSupportFeature(...args),
31
+ isEmailUnsubscribeTagMandatory: jest.fn(() => false),
29
32
  }));
30
33
 
31
34
  jest.mock('../../../../utils/history', () => ({
@@ -417,7 +420,7 @@ describe('EmailHTMLEditor', () => {
417
420
  jest.clearAllMocks();
418
421
  validateLiquidTemplateContent.mockResolvedValue(true);
419
422
  validateTags.mockReturnValue({ valid: true });
420
- isEmailUnsubscribeTagOptional.mockReturnValue(false);
423
+ isEmailUnsubscribeTagMandatory.mockReturnValue(false);
421
424
  // Reset mock functions
422
425
  mockGetAllIssues.mockReturnValue([]);
423
426
  mockGetValidationState.mockReturnValue({
@@ -425,6 +428,88 @@ describe('EmailHTMLEditor', () => {
425
428
  hasErrors: false,
426
429
  issueCounts: { errors: 0, warnings: 0, total: 0 },
427
430
  });
431
+ // Reset hasLiquidSupportFeature mock to return true by default
432
+ mockHasLiquidSupportFeature.mockReturnValue(true);
433
+ capturedApiValidationErrorsRef.current = null;
434
+ });
435
+
436
+ describe('mergedApiValidationErrors (lines 124-125)', () => {
437
+ beforeEach(() => {
438
+ global.__captureApiValidationErrorsRef = capturedApiValidationErrorsRef;
439
+ });
440
+
441
+ afterEach(() => {
442
+ delete global.__captureApiValidationErrorsRef;
443
+ });
444
+
445
+ it('merges tag unsupported errors into standardErrors when tagValidationError has unsupportedTags', async () => {
446
+ validateTags.mockReturnValue({
447
+ valid: false,
448
+ unsupportedTags: ['tagA', 'tagB'],
449
+ });
450
+ renderWithIntl({
451
+ metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
452
+ tags: [{ name: 'customer.name' }],
453
+ supportedTags: [],
454
+ });
455
+ const changeButton = screen.getByTestId('trigger-content-change');
456
+ await act(async () => {
457
+ fireEvent.click(changeButton);
458
+ });
459
+ await waitFor(() => {
460
+ expect(capturedApiValidationErrorsRef.current).not.toBeNull();
461
+ expect(capturedApiValidationErrorsRef.current.liquidErrors).toEqual([]);
462
+ expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
463
+ 'Unsupported tags are: tagA, tagB',
464
+ );
465
+ });
466
+ });
467
+
468
+ it('merges tag missing errors into standardErrors when tagValidationError has missingTags and unsubscribe not mandatory', async () => {
469
+ isEmailUnsubscribeTagMandatory.mockReturnValue(false);
470
+ validateTags.mockReturnValue({
471
+ valid: false,
472
+ missingTags: ['unsubscribe'],
473
+ });
474
+ renderWithIntl({
475
+ metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
476
+ tags: [{ name: 'customer.name' }],
477
+ supportedTags: [],
478
+ });
479
+ const changeButton = screen.getByTestId('trigger-content-change');
480
+ await act(async () => {
481
+ fireEvent.click(changeButton);
482
+ });
483
+ await waitFor(() => {
484
+ expect(capturedApiValidationErrorsRef.current).not.toBeNull();
485
+ expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
486
+ 'Missing tags are: unsubscribe',
487
+ );
488
+ });
489
+ });
490
+
491
+ it('uses apiValidationErrors.liquidErrors and concatenates apiValidationErrors.standardErrors with tag messages (merge shape)', async () => {
492
+ // When tag messages exist, mergedApiValidationErrors returns liquidErrors from apiValidationErrors
493
+ // and standardErrors = [...(apiValidationErrors?.standardErrors || []), ...tagMessages] (lines 124-125)
494
+ validateTags.mockReturnValue({
495
+ valid: false,
496
+ unsupportedTags: ['customTag'],
497
+ });
498
+ renderWithIntl({
499
+ metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
500
+ tags: [{ name: 'customer.name' }],
501
+ });
502
+ const changeButton = screen.getByTestId('trigger-content-change');
503
+ await act(async () => {
504
+ fireEvent.click(changeButton);
505
+ });
506
+ await waitFor(() => {
507
+ expect(capturedApiValidationErrorsRef.current).not.toBeNull();
508
+ const { liquidErrors, standardErrors } = capturedApiValidationErrorsRef.current;
509
+ expect(liquidErrors).toEqual([]);
510
+ expect(standardErrors).toContain('Unsupported tags are: customTag');
511
+ });
512
+ });
428
513
  });
429
514
 
430
515
  describe('Default Parameter Values (lines 60-63)', () => {
@@ -1021,7 +1106,7 @@ describe('EmailHTMLEditor', () => {
1021
1106
  });
1022
1107
 
1023
1108
  it('allows save when unsubscribe validation is on and tag is present', () => {
1024
- isEmailUnsubscribeTagOptional.mockReturnValue(false);
1109
+ isEmailUnsubscribeTagMandatory.mockReturnValue(false);
1025
1110
  renderWithIntl({
1026
1111
  isGetFormData: true,
1027
1112
  subject: 'Valid Subject',
@@ -1031,14 +1116,17 @@ describe('EmailHTMLEditor', () => {
1031
1116
  // Should proceed with save (validation passes)
1032
1117
  });
1033
1118
 
1034
- it('blocks save when liquid API validation fails for Email', async () => {
1035
- // Liquid is always enabled for Email; blocking validation is the liquid API path.
1036
- // validateLiquidTemplateContent is mocked, so simulate API validation failure by having the mock call onError.
1037
- validateLiquidTemplateContent.mockImplementation((content, options) => {
1038
- options.onError({ standardErrors: [], liquidErrors: ['Validation failed'] });
1039
- return Promise.resolve(false);
1119
+ it('blocks save for non-liquid orgs when tag validation fails', async () => {
1120
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1121
+ validateTags.mockReturnValue({
1122
+ valid: false,
1123
+ unsupportedTags: ['tag1'],
1124
+ missingTags: ['tag2'],
1040
1125
  });
1041
1126
  const onValidationFail = jest.fn();
1127
+ const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
1128
+
1129
+ // Set subject and content via component interactions
1042
1130
  const { rerender } = renderWithIntl({
1043
1131
  onValidationFail,
1044
1132
  isGetFormData: false,
@@ -1047,12 +1135,13 @@ describe('EmailHTMLEditor', () => {
1047
1135
  standard: [{ name: 'customer.name' }],
1048
1136
  },
1049
1137
  },
1050
- getLiquidTags: jest.fn((content, cb) => cb({ askAiraResponse: { data: [] }, isError: false })),
1138
+ getLiquidTags: null, // No liquid tags for non-liquid org
1051
1139
  });
1052
1140
  const input = screen.getByTestId('subject-input');
1053
1141
  fireEvent.change(input, { target: { value: 'Valid Subject' } });
1054
1142
  const changeButton = screen.getByTestId('trigger-content-change');
1055
1143
  fireEvent.click(changeButton);
1144
+ // Now trigger save
1056
1145
  rerender(
1057
1146
  <IntlProvider locale="en" messages={{}}>
1058
1147
  <EmailHTMLEditor
@@ -1064,10 +1153,12 @@ describe('EmailHTMLEditor', () => {
1064
1153
  standard: [{ name: 'customer.name' }],
1065
1154
  },
1066
1155
  }}
1067
- getLiquidTags={jest.fn((content, cb) => cb({ askAiraResponse: { data: [] }, isError: false }))} />
1156
+ getLiquidTags={null} />
1068
1157
  </IntlProvider>
1069
1158
  );
1159
+
1070
1160
  await waitFor(() => {
1161
+ expect(CapNotification.error).toHaveBeenCalled();
1071
1162
  expect(onValidationFail).toHaveBeenCalled();
1072
1163
  }, { timeout: 3000 });
1073
1164
  });
@@ -1093,6 +1184,7 @@ describe('EmailHTMLEditor', () => {
1093
1184
  standard: [{ name: 'customer.name' }],
1094
1185
  },
1095
1186
  },
1187
+ isLiquidEnabled: true,
1096
1188
  getLiquidTags,
1097
1189
  });
1098
1190
  const input = screen.getByTestId('subject-input');
@@ -1142,6 +1234,7 @@ describe('EmailHTMLEditor', () => {
1142
1234
  const { rerender } = renderWithIntl({
1143
1235
  isGetFormData: false,
1144
1236
  isFullMode: true,
1237
+ isLiquidEnabled: true,
1145
1238
  getLiquidTags,
1146
1239
  });
1147
1240
  const input = screen.getByTestId('subject-input');
@@ -1191,6 +1284,7 @@ describe('EmailHTMLEditor', () => {
1191
1284
  const { rerender } = renderWithIntl({
1192
1285
  isGetFormData: false,
1193
1286
  isFullMode: true,
1287
+ isLiquidEnabled: true,
1194
1288
  getLiquidTags,
1195
1289
  showLiquidErrorInFooter,
1196
1290
  onValidationFail,
@@ -1248,6 +1342,7 @@ describe('EmailHTMLEditor', () => {
1248
1342
  const { rerender } = renderWithIntl({
1249
1343
  isGetFormData: false,
1250
1344
  isFullMode: true,
1345
+ isLiquidEnabled: true,
1251
1346
  getLiquidTags,
1252
1347
  emailActions,
1253
1348
  templateName: 'New Template',
@@ -1283,6 +1378,7 @@ describe('EmailHTMLEditor', () => {
1283
1378
  });
1284
1379
 
1285
1380
  it('saves in full mode with create template', async () => {
1381
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1286
1382
  const emailActions = {
1287
1383
  ...defaultProps.emailActions,
1288
1384
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1316,6 +1412,7 @@ describe('EmailHTMLEditor', () => {
1316
1412
  });
1317
1413
 
1318
1414
  it('saves in full mode with edit template', async () => {
1415
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1319
1416
  const emailActions = {
1320
1417
  ...defaultProps.emailActions,
1321
1418
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1385,6 +1482,7 @@ describe('EmailHTMLEditor', () => {
1385
1482
  });
1386
1483
 
1387
1484
  it('handles create template error response', async () => {
1485
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1388
1486
  const emailActions = {
1389
1487
  ...defaultProps.emailActions,
1390
1488
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1419,6 +1517,7 @@ describe('EmailHTMLEditor', () => {
1419
1517
  });
1420
1518
 
1421
1519
  it('handles create template success with getFormdata', async () => {
1520
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1422
1521
  const emailActions = {
1423
1522
  ...defaultProps.emailActions,
1424
1523
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1455,6 +1554,7 @@ describe('EmailHTMLEditor', () => {
1455
1554
  });
1456
1555
 
1457
1556
  it('handles create template success without getFormdata', async () => {
1557
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1458
1558
  const emailActions = {
1459
1559
  ...defaultProps.emailActions,
1460
1560
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1490,6 +1590,7 @@ describe('EmailHTMLEditor', () => {
1490
1590
  });
1491
1591
 
1492
1592
  it('saves in library mode with getFormdata', async () => {
1593
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1493
1594
  const getFormdata = jest.fn();
1494
1595
 
1495
1596
  // Set subject and content via component interactions
@@ -1517,6 +1618,7 @@ describe('EmailHTMLEditor', () => {
1517
1618
  });
1518
1619
 
1519
1620
  it('saves in library mode without library module', async () => {
1621
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1520
1622
  const getFormdata = jest.fn();
1521
1623
 
1522
1624
  // Set subject and content via component interactions
@@ -1656,6 +1758,7 @@ describe('EmailHTMLEditor', () => {
1656
1758
  renderWithIntl({
1657
1759
  getLiquidTags: null,
1658
1760
  globalActions,
1761
+ isLiquidEnabled: true,
1659
1762
  isGetFormData: true,
1660
1763
  subject: 'Valid Subject',
1661
1764
  htmlContent: '<p>Content</p>',
@@ -22,7 +22,7 @@ import {
22
22
  CapTooltip,
23
23
  } from '@capillarytech/cap-ui-library';
24
24
  import { FONT_SIZE_L } from '@capillarytech/cap-ui-library/styled/variables';
25
- import _, { find, cloneDeep, findIndex, isEmpty, isEqual, filter, replace } from 'lodash';
25
+ import _, { find, cloneDeep, findIndex, isEmpty, isEqual, filter, flattenDeep, replace } from 'lodash';
26
26
  import * as actions from './actions';
27
27
  import { makeSelectFTP, makeSelectMetaEntities } from './selectors';
28
28
  import { makeSelectLoyaltyPromotionDisplay, setInjectedTags } from '../Cap/selectors';
@@ -33,6 +33,7 @@ import * as globalActions from '../Cap/actions';
33
33
  import { TagList } from '../TagList';
34
34
  import { NO_COMMUNICATION, CREATE, EDIT, PREVIEW } from '../App/constants';
35
35
  import { getTreeStructuredTags } from '../../utils/common';
36
+ import { transformInjectedTags, checkIfSupportedTag, skipTags } from '../../utils/tagValidations';
36
37
  import injectSaga from '../../utils/injectSaga';
37
38
  import injectReducer from '../../utils/injectReducer';
38
39
 
@@ -234,16 +235,63 @@ export class FTP extends React.Component {
234
235
  }));
235
236
  };
236
237
 
238
+ getFlatTags = (tags) => {
239
+ const flatTags = [];
240
+ tags.forEach((tag) => {
241
+ if ((tag.children || []).length) {
242
+ const innerTags = this.getFlatTags(tag.children);
243
+ flatTags.push(innerTags);
244
+ } else {
245
+ flatTags.push(tag.value);
246
+ }
247
+ });
248
+
249
+ return flattenDeep(flatTags);
250
+ };
251
+
252
+ validateTags(content, tagsParam, injectedTagsParams) {
253
+ const tags = tagsParam;
254
+ const injectedTags = transformInjectedTags(injectedTagsParams);
255
+ const response = {};
256
+ response.valid = true;
257
+ response.unsupportedTags = [];
258
+ const flatTags = this.getFlatTags(tags);
259
+ if (flatTags && flatTags.length) {
260
+ const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
261
+ const matchedTags = [...content.matchAll(regex)];
262
+ matchedTags.forEach((tag) => {
263
+ let ifSupported = !!flatTags.find((t) => t === tag[0]);
264
+ const tagValue = tag[0].substring(this.indexOfEnd(tag[0], '{{'), tag[0].indexOf('}}'));
265
+ ifSupported = ifSupported || checkIfSupportedTag(tagValue, injectedTags) || skipTags(tagValue);
266
+ if (!ifSupported) {
267
+ response.unsupportedTags.push(tagValue);
268
+ response.valid = false;
269
+ }
270
+ if (response.unsupportedTags.length === 0) {
271
+ response.valid = true;
272
+ }
273
+ });
274
+ }
275
+ return response;
276
+ }
277
+
237
278
  indexOfEnd(targetString, string) {
238
279
  const io = targetString.indexOf(string);
239
280
  return io == -1 ? -1 : io + string.length;
240
281
  }
241
282
 
242
283
  getMessageContent = () => {
243
- const { messageContent } = this.state;
284
+ const { messageContent, tagsTree = []} = this.state;
244
285
  const { formatMessage } = this.props.intl;
245
286
  const { metaEntities, selectedOfferDetails, injectedTags } = this.props;
246
287
  const tagsRaw = metaEntities && metaEntities.tags ? metaEntities.tags.standard : [];
288
+ const validateTagResponse = !this.props?.isFullMode ? this.validateTags(messageContent, tagsTree, injectedTags) : { valid: true, unsupportedTags: [] };
289
+ let unsupportedTags = null;
290
+ let errorMessageText = '';
291
+ if (!validateTagResponse.valid) {
292
+ unsupportedTags = validateTagResponse.unsupportedTags.join(', ').toString();
293
+ errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags});
294
+ }
247
295
  return (
248
296
  <CapRow>
249
297
  <CapColumn span={11}>
@@ -261,6 +309,7 @@ export class FTP extends React.Component {
261
309
  label={formatMessage(messages.messageHeader)}
262
310
  onChange={this.updateMessageBody}
263
311
  value={messageContent}
312
+ errorMessage={errorMessageText}
264
313
  />
265
314
  </div>
266
315
  </CapColumn>
@@ -21,6 +21,10 @@ export default defineMessages({
21
21
  id: 'creatives.containersV2.FTP.addColumn',
22
22
  defaultMessage: 'Add column',
23
23
  },
24
+ unsupportedTagsValidationError: {
25
+ id: 'creatives.containersV2.FTP.unsupportedTagsValidationError',
26
+ defaultMessage: 'Unsupported tags: {unsupportedTags}. Please remove them from this message.',
27
+ },
24
28
  selectTag: {
25
29
  id: 'creatives.containersV2.FTP.selectTag',
26
30
  defaultMessage: 'Select tag',
@@ -31,6 +31,7 @@ import creativesMessages from '../CreativesContainer/messages';
31
31
  import withCreatives from "../../hoc/withCreatives";
32
32
  import UnifiedPreview from "../../v2Components/CommonTestAndPreview/UnifiedPreview";
33
33
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
34
+ import { validateTags } from "../../utils/tagValidations";
34
35
  import injectReducer from '../../utils/injectReducer';
35
36
  import v2InAppReducer from './reducer';
36
37
  import { v2InAppSagas } from './sagas';
@@ -57,6 +58,7 @@ import {
57
58
  import { getCdnUrl } from "../../utils/cdnTransformation";
58
59
  import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
59
60
  import { validateInAppContent } from "../../utils/commonUtils";
61
+ import { hasLiquidSupportFeature } from "../../utils/common";
60
62
  import formBuilderMessages from "../../v2Components/FormBuilder/messages";
61
63
  import HTMLEditor from "../../v2Components/HtmlEditor";
62
64
  import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
@@ -1093,7 +1095,9 @@ export const InApp = (props) => {
1093
1095
  // Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
1094
1096
  const hasTags = tags && tags.length > 0;
1095
1097
 
1096
- if (hasTags) {
1098
+ // For liquid flow, use validateInAppContent
1099
+ if (isLiquidFlow && hasTags) {
1100
+ // Validate the INAPP content
1097
1101
  const payload = createPayload();
1098
1102
  validateInAppContent(payload, {
1099
1103
  currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
@@ -1102,6 +1106,10 @@ export const InApp = (props) => {
1102
1106
  getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
1103
1107
  formatMessage,
1104
1108
  messages: formBuilderMessages,
1109
+ tagLookupMap: metaEntities?.tagLookupMap || {},
1110
+ eventContextTags: metaEntities?.eventContextTags || [],
1111
+ isLiquidFlow,
1112
+ forwardedTags: {},
1105
1113
  skipTags: (tag) => {
1106
1114
  // Skip certain tags if needed
1107
1115
  const skipRegexes = [
@@ -1116,11 +1124,98 @@ export const InApp = (props) => {
1116
1124
  },
1117
1125
  singleTab: getSingleTab(accountData),
1118
1126
  });
1127
+ } else if (hasTags) {
1128
+ // For non-liquid flow, validate tags using validateTags (only if tags are available)
1129
+ const androidContent = htmlContentAndroid || '';
1130
+ const iosContent = htmlContentIos || '';
1131
+
1132
+ const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
1133
+ const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
1134
+
1135
+ let hasErrors = false;
1136
+ const newErrors = {
1137
+ STANDARD_ERROR_MSG: {
1138
+ ANDROID: [],
1139
+ IOS: [],
1140
+ GENERIC: [],
1141
+ },
1142
+ LIQUID_ERROR_MSG: {
1143
+ ANDROID: [],
1144
+ IOS: [],
1145
+ GENERIC: [],
1146
+ },
1147
+ };
1148
+
1149
+ // Validate Android content
1150
+ if (androidSupported && androidContent && androidContent?.trim() !== '') {
1151
+ const validationResponse = validateTags({
1152
+ content: androidContent,
1153
+ tagsParam: tags,
1154
+ injectedTagsParams: injectedTags || {},
1155
+ location,
1156
+ tagModule: getDefaultTags,
1157
+ eventContextTags: metaEntities?.eventContextTags || [],
1158
+ isFullMode,
1159
+ }) || {};
1160
+
1161
+ if (validationResponse?.unsupportedTags?.length > 0) {
1162
+ hasErrors = true;
1163
+ newErrors.LIQUID_ERROR_MSG.ANDROID.push(
1164
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1165
+ unsupportedTags: validationResponse.unsupportedTags.join(", "),
1166
+ })
1167
+ );
1168
+ }
1169
+ if (validationResponse?.isBraceError) {
1170
+ hasErrors = true;
1171
+ newErrors.LIQUID_ERROR_MSG.ANDROID.push(
1172
+ formatMessage(globalMessages.unbalanacedCurlyBraces)
1173
+ );
1174
+ }
1175
+ }
1176
+
1177
+ // Validate iOS content
1178
+ if (iosSupported && iosContent && iosContent?.trim() !== '') {
1179
+ const validationResponse = validateTags({
1180
+ content: iosContent,
1181
+ tagsParam: tags,
1182
+ injectedTagsParams: injectedTags || {},
1183
+ location,
1184
+ tagModule: getDefaultTags,
1185
+ eventContextTags: metaEntities?.eventContextTags || [],
1186
+ isFullMode,
1187
+ }) || {};
1188
+
1189
+ if (validationResponse?.unsupportedTags?.length > 0) {
1190
+ hasErrors = true;
1191
+ newErrors.LIQUID_ERROR_MSG.IOS.push(
1192
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1193
+ unsupportedTags: validationResponse.unsupportedTags.join(", "),
1194
+ })
1195
+ );
1196
+ }
1197
+ if (validationResponse?.isBraceError) {
1198
+ hasErrors = true;
1199
+ newErrors.LIQUID_ERROR_MSG.IOS.push(
1200
+ formatMessage(globalMessages.unbalanacedCurlyBraces)
1201
+ );
1202
+ }
1203
+ }
1204
+
1205
+ if (hasErrors) {
1206
+ setErrorMessage(newErrors);
1207
+ } else {
1208
+ // No errors, proceed with submission
1209
+ onSuccess();
1210
+ }
1119
1211
  } else {
1212
+ // No tags available, skip validation and proceed directly
1120
1213
  onSuccess();
1121
1214
  }
1122
1215
  };
1123
1216
 
1217
+ const isLiquidFlow = hasLiquidSupportFeature();
1218
+
1124
1219
  // Check template data to determine editor type (for render decision)
1125
1220
  const templateDetails = isFullMode ? editData?.templateDetails : templateData;
1126
1221
  const versions = templateDetails?.versions || {};
@@ -21,16 +21,18 @@ import { getCtaObject } from '../utils';
21
21
  jest.mock('redux-auth-wrapper/history4/redirect', () => ({
22
22
  connectedRouterRedirect: jest.fn((config) => (Component) => Component),
23
23
  }));
24
- import * as commonUtils from '../../../utils/commonUtils';
25
24
 
26
25
  const mockActions = {
27
26
  getTemplateInfoById: jest.fn(),
28
27
  resetEditTemplate: jest.fn(),
29
- getTemplateDetails: jest.fn((id, setSpin) => {
28
+ getTemplateDetails: jest.fn((id, callback) => {
30
29
  // Simulate successful template details fetch to prevent loading state
31
30
  // The callback is setSpin function, call it with false to stop spinner
32
- if (setSpin && typeof setSpin === 'function') {
33
- setTimeout(() => setSpin(false), 0);
31
+ if (callback && typeof callback === 'function') {
32
+ // Use setTimeout to ensure it's called after render
33
+ setTimeout(() => {
34
+ callback(false);
35
+ }, 0);
34
36
  }
35
37
  }),
36
38
  editTemplate: jest.fn(),
@@ -39,9 +41,6 @@ const mockActions = {
39
41
  };
40
42
  const mockGlobalActions = {
41
43
  fetchSchemaForEntity: jest.fn(),
42
- getLiquidTags: jest.fn((content, callback) =>
43
- callback({ askAiraResponse: { data: [], errors: [] }, isError: false }),
44
- ),
45
44
  };
46
45
 
47
46
  jest.mock('../../../v2Containers/TagList/index.js', () => ({
@@ -67,17 +66,7 @@ const renderComponent = (props) =>
67
66
  );
68
67
 
69
68
  describe('Test activity inApp container', () => {
70
- afterEach(() => {
71
- jest.restoreAllMocks();
72
- });
73
-
74
69
  it('test case for inApp template update flow', async () => {
75
- jest
76
- .spyOn(commonUtils, 'validateInAppContent')
77
- .mockImplementation((payload, options) => {
78
- options.onSuccess();
79
- return Promise.resolve(true);
80
- });
81
70
  renderComponent({
82
71
  actions: mockActions,
83
72
  globalActions: mockGlobalActions,