@capillarytech/creatives-library 8.0.285 → 8.0.287-alpha.0

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 (42) hide show
  1. package/constants/unified.js +0 -1
  2. package/initialState.js +0 -2
  3. package/package.json +1 -1
  4. package/utils/common.js +5 -8
  5. package/utils/commonUtils.js +2 -83
  6. package/utils/tagValidations.js +84 -222
  7. package/utils/tests/commonUtil.test.js +147 -118
  8. package/utils/tests/tagValidations.test.js +280 -358
  9. package/v2Components/ErrorInfoNote/index.js +2 -5
  10. package/v2Components/FormBuilder/index.js +64 -158
  11. package/v2Components/FormBuilder/messages.js +0 -8
  12. package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
  15. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
  16. package/v2Containers/Cap/mockData.js +0 -14
  17. package/v2Containers/Cap/reducer.js +3 -55
  18. package/v2Containers/Cap/tests/reducer.test.js +0 -102
  19. package/v2Containers/CreativesContainer/index.js +0 -1
  20. package/v2Containers/Email/index.js +1 -5
  21. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +10 -62
  22. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +12 -115
  23. package/v2Containers/FTP/index.js +2 -51
  24. package/v2Containers/FTP/messages.js +0 -4
  25. package/v2Containers/InApp/index.js +1 -96
  26. package/v2Containers/InApp/tests/index.test.js +17 -6
  27. package/v2Containers/InappAdvance/index.js +2 -103
  28. package/v2Containers/Line/Container/Text/index.js +0 -1
  29. package/v2Containers/MobilePushNew/index.js +2 -33
  30. package/v2Containers/Rcs/index.js +12 -37
  31. package/v2Containers/SmsTrai/Create/index.scss +1 -1
  32. package/v2Containers/SmsTrai/Edit/index.js +6 -47
  33. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  34. package/v2Containers/Viber/index.js +0 -1
  35. package/v2Containers/Viber/index.scss +1 -1
  36. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
  37. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  38. package/v2Containers/WebPush/Create/index.js +2 -2
  39. package/v2Containers/WebPush/Create/utils/validation.js +18 -9
  40. package/v2Containers/WebPush/Create/utils/validation.test.js +0 -24
  41. package/v2Containers/Whatsapp/index.js +9 -17
  42. package/v2Containers/Zalo/index.js +3 -11
@@ -186,7 +186,6 @@ export const ErrorInfoNote = (props) => {
186
186
  errorMessages,
187
187
  onErrorClick,
188
188
  onClose,
189
- isLiquidEnabled = true,
190
189
  intl,
191
190
  useLegacyDisplay = false, // Use simple list display instead of tabs (for BEE Editor)
192
191
  } = props;
@@ -230,7 +229,7 @@ export const ErrorInfoNote = (props) => {
230
229
  const standardErrors = Array.isArray(rawStandardErrors) ? rawStandardErrors : [];
231
230
  const liquidErrors = Array.isArray(rawLiquidErrors) ? rawLiquidErrors : [];
232
231
  const hasStandardErrors = standardErrors.length > 0;
233
- const hasLiquidErrors = liquidErrors.length > 0 && isLiquidEnabled;
232
+ const hasLiquidErrors = liquidErrors.length > 0;
234
233
 
235
234
  if (!hasStandardErrors && !hasLiquidErrors) {
236
235
  return null;
@@ -357,7 +356,7 @@ export const ErrorInfoNote = (props) => {
357
356
  className="error-info-note__tabs"
358
357
  />
359
358
  <CapRow className="error-info-note__actions">
360
- {hasLiquidErrors && isLiquidEnabled && (
359
+ {hasLiquidErrors && (
361
360
  <CapButton
362
361
  type="flat"
363
362
  className="error-info-note__liquid-doc"
@@ -452,7 +451,6 @@ ErrorInfoNote.defaultProps = {
452
451
  },
453
452
  onErrorClick: null,
454
453
  onClose: null,
455
- isLiquidEnabled: true,
456
454
  intl: null,
457
455
  useLegacyDisplay: false, // Use simple list display for BEE Editor
458
456
  };
@@ -479,7 +477,6 @@ ErrorInfoNote.propTypes = {
479
477
  }),
480
478
  onErrorClick: PropTypes.func,
481
479
  onClose: PropTypes.func,
482
- isLiquidEnabled: PropTypes.bool,
483
480
  intl: PropTypes.object,
484
481
  useLegacyDisplay: PropTypes.bool, // Use simple list display for BEE Editor
485
482
  };
@@ -50,7 +50,7 @@ import { makeSelectMetaEntities, selectCurrentOrgDetails, selectLiquidStateDetai
50
50
  import * as actions from "../../v2Containers/Cap/actions";
51
51
  import './_formBuilder.scss';
52
52
  import {updateCharCount, checkUnicode} from "../../utils/smsCharCountV2";
53
- import { checkSupport, extractNames, preprocessHtml, validateIfTagClosed, isInsideLiquidBlock} from '../../utils/tagValidations';
53
+ import { preprocessHtml, validateTagsCore, hasUnsubscribeTag } from '../../utils/tagValidations';
54
54
  import { containsBase64Images } from '../../utils/content';
55
55
  import { SMS, MOBILE_PUSH, LINE, ENABLE_AI_SUGGESTIONS,AI_CONTENT_BOT_DISABLED, EMAIL, LIQUID_SUPPORTED_CHANNELS, INAPP } from '../../v2Containers/CreativesContainer/constants';
56
56
  import globalMessages from '../../v2Containers/Cap/messages';
@@ -60,7 +60,7 @@ import { GET_TRANSLATION_MAPPED } from '../../constants/unified';
60
60
  import moment from 'moment';
61
61
  import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS} from '../../constants/unified';
62
62
  import { REQUEST } from '../../v2Containers/Cap/constants'
63
- import { hasLiquidSupportFeature, isEmailUnsubscribeTagMandatory } from '../../utils/common';
63
+ import { isEmailUnsubscribeTagOptional } from '../../utils/common';
64
64
  import { isUrl } from '../../v2Containers/Line/Container/Wrapper/utils';
65
65
  import { bindActionCreators } from 'redux';
66
66
  import { getChannelData, validateLiquidTemplateContent, validateMobilePushContent } from '../../utils/commonUtils';
@@ -72,10 +72,8 @@ const {CapRadioGroup} = CapRadio;
72
72
 
73
73
  const tagsTypes = {
74
74
  MISSING_TAGS: 'missingTags',
75
- UNSUPPORTED_TAGS: 'unsupportedTags',
76
75
  };
77
76
  const errorMessageForTags = {
78
- UNSUPPORTED_TAG_ERROR: 'unsupportedTagsError',
79
77
  MISSING_TAG_ERROR: 'missingTagsError',
80
78
  GENERIC_VALIDATION_ERROR: 'genericValidationError',
81
79
  TAG_BRACKET_COUNT_MISMATCH_ERROR: 'tagBracketCountMismatchError'
@@ -138,7 +136,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
138
136
  this.handleSetRadioValue = this.handleSetRadioValue.bind(this);
139
137
  this.formElements = [];
140
138
  // Check if the liquid flow feature is supported and the channel is in the supported list.
141
- this.liquidFlow = this.isLiquidFlowSupported.bind(this);
139
+ this.isLiquidFlowSupportedByChannel = this.isLiquidFlowSupportedByChannel.bind(this);
142
140
  this.onSubmitWrapper = this.onSubmitWrapper.bind(this);
143
141
 
144
142
  // Performance optimization: Debounced functions for high-frequency updates
@@ -330,8 +328,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
330
328
  return updatedFormData;
331
329
  }
332
330
 
333
- isLiquidFlowSupported = () => {
334
- return Boolean(LIQUID_SUPPORTED_CHANNELS.includes(this.props?.schema?.channel?.toUpperCase()) && hasLiquidSupportFeature());
331
+ isLiquidFlowSupportedByChannel = () => {
332
+ return Boolean(LIQUID_SUPPORTED_CHANNELS.includes(this.props?.schema?.channel?.toUpperCase()));
335
333
  }
336
334
 
337
335
  componentWillMount() {
@@ -726,17 +724,19 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
726
724
 
727
725
  let tagValidationResponse = false;
728
726
  if (content) {
729
- tagValidationResponse = this.validateTags(content, tags, injectedTags, false, this.props?.isFullMode);
727
+ tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
730
728
  }
731
-
732
- if (tagValidationResponse.valid) {
729
+
730
+ const tagResult = tagValidationResponse && typeof tagValidationResponse === 'object'
731
+ ? tagValidationResponse
732
+ : { valid: false, missingTags: [], isBraceError: false };
733
+ if (tagResult.valid) {
733
734
  errorData[count][`sms-editor${index > 1 ? index : ''}`] = false;
734
735
  } else {
735
- errorData[count]['invalid-tags'] = tagValidationResponse.unsupportedTags;
736
- const { MISSING_TAG_ERROR, UNSUPPORTED_TAG_ERROR, GENERIC_VALIDATION_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags || {};
737
- const { missingTags, unsupportedTags, isBraceError} = tagValidationResponse;
738
- errorData[count][`sms-editor${index > 1 ? index : ''}`] = missingTags && missingTags.length ? MISSING_TAG_ERROR : ( unsupportedTags && unsupportedTags.length ? UNSUPPORTED_TAG_ERROR : (isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : GENERIC_VALIDATION_ERROR));
739
- errorData[count][`bracket-error`] = tagValidationResponse.isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
736
+ const { MISSING_TAG_ERROR, GENERIC_VALIDATION_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags || {};
737
+ const { missingTags, isBraceError } = tagResult;
738
+ errorData[count][`sms-editor${index > 1 ? index : ''}`] = missingTags && missingTags.length ? MISSING_TAG_ERROR : (isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : GENERIC_VALIDATION_ERROR);
739
+ errorData[count][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
740
740
  isValid = false;
741
741
  }
742
742
  if(content !== '' && (ifUnicode && !unicodeCheck)) {
@@ -764,7 +764,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
764
764
  if (this.state.formData['message-editor'] !== undefined ) {
765
765
  const content = this.state.formData['0']['message-editor'] || '';
766
766
 
767
- const tagValidationResponse = this.validateTags((content), tags, injectedTags, false, this.props?.isFullMode);
767
+ const tagValidationResponse = this.validateTags((content), tags, false, this.props?.isFullMode);
768
768
 
769
769
  if (tagValidationResponse.valid) {
770
770
  errorData = {
@@ -847,7 +847,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
847
847
  errorData[index] = true;
848
848
  isValid = false;
849
849
  } else {
850
- const tagValidationResponse = this.validateTags(content, tags, injectedTags, false, this.props?.isFullMode);
850
+ const tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
851
851
 
852
852
  if (tagValidationResponse.valid) {
853
853
  errorData[index] = false;
@@ -910,14 +910,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
910
910
  isCurrentTabValid = false;
911
911
  } else {
912
912
  errorData[parseInt(index)][`message-editor${selector}`] = false;
913
- const tagValidationResponse = this.validateTags(message, tags, injectedTags, false, this.props?.isFullMode);
913
+ const tagValidationResponse = this.validateTags(message, tags, false, this.props?.isFullMode);
914
914
 
915
915
  if (tagValidationResponse.valid) {
916
916
  errorData[parseInt(index)][`message-editor${selector}`] = false;
917
917
  } else {
918
- const {isBraceError} = tagValidationResponse;
918
+ const { isBraceError } = tagValidationResponse;
919
919
  errorData[parseInt(index)][`message-editor${selector}`] = isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : true;
920
- errorData[parseInt(index)]['invalid-tags'] = tagValidationResponse.unsupportedTags;
921
920
  errorData[parseInt(index)][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
922
921
  isValid = false;
923
922
  }
@@ -939,7 +938,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
939
938
  isCurrentTabValid = false;
940
939
  } else {
941
940
  errorData[parseInt(index)][`message-title${selector}`] = false;
942
- const tagValidationResponse = this.validateTags(title, tags, injectedTags, false, this.props?.isFullMode);
941
+ const tagValidationResponse = this.validateTags(title, tags, false, this.props?.isFullMode);
943
942
 
944
943
  if (tagValidationResponse.valid) {
945
944
  errorData[parseInt(index)][`message-title${selector}`] = false;
@@ -1193,8 +1192,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1193
1192
  if (!content) {
1194
1193
  return false;
1195
1194
  }
1196
- const tagValidationResponse = this.validateTags(content, tags, injectedTags, isEmail, this.props?.isFullMode);
1197
-
1195
+ const tagValidationResponse = this.validateTags(content, tags, isEmail, this.props?.isFullMode);
1198
1196
  // Check for base64 images in email content
1199
1197
  isEmail && containsBase64Images({content, callback:()=>{
1200
1198
  tagValidationResponse.valid = false;
@@ -1210,23 +1208,20 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1210
1208
  errorData[index][currentLang]['template-content'] = true;
1211
1209
  isValid = false;
1212
1210
  isLiquidValid = false;
1213
- if ((showMessages && !isNaN(index)) || this.liquidFlow()) {
1214
- if (tagValidationResponse?.missingTags?.length > 0 || tagValidationResponse?.unsupportedTags?.length > 0) {
1211
+ if ((showMessages && !isNaN(index)) || this.isLiquidFlowSupportedByChannel()) {
1212
+ if (tagValidationResponse?.missingTags?.length > 0) {
1215
1213
  errorString += `${this.props.intl.formatMessage(messages.contentNotValidLanguage)} ${currentLang}\n`;
1216
1214
  }
1217
1215
  if (tagValidationResponse?.missingTags?.length > 0) {
1218
1216
  errorString += `${this.props.intl.formatMessage(messages.missingTags)} ${tagValidationResponse.missingTags.toString()}\n`;
1219
1217
  }
1220
- if (tagValidationResponse?.unsupportedTags?.length > 0) {
1221
- errorString += `${this.props.intl.formatMessage(messages.unsupportedTags)} ${tagValidationResponse.unsupportedTags.toString()}\n`;
1222
- }
1223
1218
  if (tagValidationResponse?.isBraceError){
1224
1219
  errorString += this.props.intl.formatMessage(globalMessages.unbalanacedCurlyBraces);
1225
1220
  }
1226
1221
  if (tagValidationResponse?.isContentEmpty) {
1227
1222
  errorString += this.props.intl.formatMessage(messages.emailBodyEmptyError);
1228
1223
  // Adds a bypass for cases where content is initially empty in the creation flow.
1229
- if(this.liquidFlow()){
1224
+ if(this.isLiquidFlowSupportedByChannel()){
1230
1225
  errorString = "";
1231
1226
  isLiquidValid = true;
1232
1227
  }
@@ -1238,7 +1233,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1238
1233
  }
1239
1234
  }
1240
1235
  if (errorString) {
1241
- if (this.liquidFlow()) {
1236
+ if (this.isLiquidFlowSupportedByChannel()) {
1242
1237
  this.setState(
1243
1238
  (prevState) => ({
1244
1239
  liquidErrorMessage: {
@@ -1265,7 +1260,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1265
1260
  });
1266
1261
  }
1267
1262
 
1268
- const isTemplateValid = this.liquidFlow() ? isLiquidValid : isValid;
1263
+ const isTemplateValid = this.isLiquidFlowSupportedByChannel() ? isLiquidValid : isValid;
1269
1264
  //Updating the state with the error data
1270
1265
  this.setState((prevState) => ({
1271
1266
  isFormValid: isTemplateValid,
@@ -1324,7 +1319,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1324
1319
  }
1325
1320
  onSubmitWrapper = (args) => {
1326
1321
  const {singleTab = null} = args || {};
1327
- if (this.liquidFlow()) {
1322
+ if (this.isLiquidFlowSupportedByChannel()) {
1328
1323
  // For MPUSH, we need to validate both Android and iOS content separately
1329
1324
  if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
1330
1325
  this.validateFormBuilderMPush(this.state.formData, singleTab);
@@ -1368,10 +1363,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1368
1363
  messages,
1369
1364
  onError,
1370
1365
  onSuccess,
1371
- tagLookupMap: this.props?.metaEntities?.tagLookupMap,
1372
- eventContextTags: this.props?.eventContextTags,
1373
- isLiquidFlow: this.liquidFlow(),
1374
- forwardedTags: this.props?.isLoyaltyModule ? this.props?.forwardedTags : {},
1375
1366
  skipTags: this.skipTags.bind(this)
1376
1367
  });
1377
1368
  } else {
@@ -1440,13 +1431,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1440
1431
  getLiquidTags: this.props.actions.getLiquidTags,
1441
1432
  formatMessage: this.props.intl.formatMessage,
1442
1433
  messages: messages,
1443
- tagLookupMap: this.props?.metaEntities?.tagLookupMap,
1444
- eventContextTags: this.props?.eventContextTags,
1445
- isLiquidFlow: this.liquidFlow(), // Use the method instead of props
1446
- forwardedTags: this.props?.isLoyaltyModule ? this.props?.forwardedTags : {},
1447
- skipTags: this.skipTags.bind(this),
1448
- extractNames,
1449
- checkSupport,
1450
1434
  singleTab: singleTab?.toUpperCase(),
1451
1435
  });
1452
1436
  }
@@ -1501,115 +1485,40 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1501
1485
  });
1502
1486
  }
1503
1487
 
1504
- validateTags(content, tagsParam, injectedTagsParams, isEmail = false, isFullMode = this.props?.isFullMode) {
1505
- const type = (this.props.location && this.props.location.query.type) ? this.props.location.query.type : 'full';
1488
+ validateTags(content, tagsParam, isEmail = false, isFullMode = this.props?.isFullMode) {
1506
1489
  let currentModule = this.props.location.query.module ? this.props.location.query.module : 'default';
1507
1490
  if (this.props.tagModule) {
1508
1491
  currentModule = this.props.tagModule;
1509
1492
  }
1510
1493
  const tags = tagsParam ? tagsParam : this.props.tags;
1511
- const injectedTags = this.transformInjectedTags(injectedTagsParams ? injectedTagsParams : this.props.injectedTags);
1512
- const excludedTags = ['user_id_b64', 'outbox_id_b64'];
1513
-
1514
-
1515
- const response = {};
1516
- response.valid = true;
1517
- response.missingTags = [];
1518
- response.unsupportedTags = [];
1519
- response.isBraceError = false;
1520
- response.isContentEmpty = false;
1521
- const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content ;
1522
- const isModuleTypeOutbound = (this.props?.moduleType || '').toUpperCase() === OUTBOUND;
1523
- // Run tag validation (missing + unsupported): library mode, or full mode with liquid support, or
1524
- // legacy Email (CK Editor) when unsubscribe is required (EMAIL_UNSUBSCRIBE_TAG_MANDATORY false) so missing-unsubscribe error shows
1525
- const shouldRunTagValidation = !isFullMode
1526
- || (isEmail && hasLiquidSupportFeature())
1527
- || (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound);
1528
- if (tags && tags.length && !isFullMode) {
1529
- _.forEach(tags, (tag) => {
1530
- _.forEach(tag.definition.supportedModules, (module) => {
1531
- if (module.mandatory && (currentModule === module.context)) {
1532
- if (content.indexOf(`{{${tag.definition.value}}}`) === -1) {
1533
- response.valid = false;
1534
- response.missingTags.push(tag.definition.value);
1535
- }
1536
- }
1537
- });
1538
- });
1539
- // Legacy Email (CK Editor): when unsubscribe is required, ensure we validate it even if tag schema didn't mark it mandatory for this module
1540
- if (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
1541
- const hasUnsubscribeInContent = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g.test(content);
1542
- if (!hasUnsubscribeInContent && response.missingTags.indexOf('unsubscribe') === -1) {
1543
- response.valid = false;
1544
- response.missingTags.push('unsubscribe');
1545
- }
1546
- }
1547
- const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
1548
- let match = regex.exec(content);
1549
- const regexImgSrc=/<img[^>]*\bsrc\s*=\s*"[^"]*{{customer_barcode}}[^"]*"/;
1550
- let matchImg = regexImgSrc.exec(content);
1551
- const regexCustomerBarcode = /{{customer_barcode}}(?![^<]*>)/g;
1552
- let matchCustomerBarcode = regexCustomerBarcode.exec(content);
1553
- // \S matches anything other than a space, a tab, a newline, or a carriage return.
1554
- const validString= /\S/.test(contentForValidation);
1555
- if (isEmailUnsubscribeTagMandatory() && isEmail && isModuleTypeOutbound) {
1556
- const missingTagIndex = response?.missingTags?.indexOf("unsubscribe");
1557
- if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
1558
- response?.missingTags?.splice(missingTagIndex, 1);
1559
- }
1560
- if (validString) {
1561
- response.valid = true;
1562
- } else {
1563
- response.isContentEmpty = true;
1564
- }
1565
- }
1566
- while (match !== null ) {
1567
- const tagValue = match[0].substring(this.indexOfEnd(match[0], '{{'), match[0].indexOf('}}'));
1568
- const tagIndex = match?.index;
1569
- match = regex.exec(content);
1570
- let ifSupported = false;
1571
- _.forEach(tags, (tag) => {
1572
- if (tag.definition.value === tagValue) {
1573
- ifSupported = true;
1574
- }
1575
- if(tagValue === CUSTOMER_BARCODE_TAG && (matchImg === null || matchCustomerBarcode !== null)){
1576
- ifSupported = false;
1577
- }
1578
- });
1579
- const ifSkipped = this.skipTags(tagValue);
1580
- if (ifSkipped) {
1581
- ifSupported = true;
1582
- let isUnsubscribeSkipped = tagValue.indexOf("unsubscribe") != -1 ;
1583
- if (isUnsubscribeSkipped) {
1584
- const missingTagIndex = response.missingTags.indexOf("unsubscribe");
1585
- if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
1586
- response.missingTags.splice(missingTagIndex, 1);
1587
- }
1588
- }
1589
- }
1590
-
1591
- // Event Context Tags support
1592
- this.props?.eventContextTags?.forEach((tag) => {
1593
- if (tagValue === tag?.tagName) {
1594
- ifSupported = true;
1595
- }
1596
- });
1597
-
1598
- ifSupported = ifSupported || this.checkIfSupportedTag(tagValue, injectedTags);
1599
- // Only add to unsupportedTags if not inside a {% ... %} block (scenario 3: liquid orgs also get unsupported-tag errors)
1600
- if (!ifSupported && !isInsideLiquidBlock(content, tagIndex)) {
1601
- response.unsupportedTags.push(tagValue);
1602
- response.valid = false;
1603
- }
1494
+ const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content;
1495
+ const isOutboundModule = (currentModule || '').toUpperCase() === OUTBOUND;
1496
+
1497
+ const initialMissingTags = (tags && tags.length && !isFullMode && isEmail && isOutboundModule && !isEmailUnsubscribeTagOptional() && !hasUnsubscribeTag(content))
1498
+ ? ['unsubscribe']
1499
+ : [];
1500
+
1501
+ const response = validateTagsCore({
1502
+ contentForBraceCheck: contentForValidation,
1503
+ contentForUnsubscribeScan: content,
1504
+ tags,
1505
+ currentModule,
1506
+ isFullMode,
1507
+ initialMissingTags, // [] or ['unsubscribe']; core uses this instead of definition-based when provided
1508
+ skipTagsFn: this.skipTags.bind(this),
1509
+ includeIsContentEmpty: true,
1510
+ });
1604
1511
 
1605
- if (response?.unsupportedTags?.length == 0 && response?.missingTags?.length == 0 ) {
1606
- response.valid = true;
1607
- }
1512
+ // When unsubscribe tag is optional (isEmailUnsubscribeTagOptional): do not enforce unsubscribe (defensive splice); set isContentEmpty only when content is empty. Do not override response.valid so brace/format errors are preserved.
1513
+ const validString = /\S/.test(contentForValidation);
1514
+ if (isEmailUnsubscribeTagOptional() && isEmail && isOutboundModule) {
1515
+ const missingTagIndex = response.missingTags.indexOf('unsubscribe');
1516
+ if (missingTagIndex !== -1) {
1517
+ response.missingTags.splice(missingTagIndex, 1);
1518
+ }
1519
+ if (!validString) {
1520
+ response.isContentEmpty = true;
1608
1521
  }
1609
- }
1610
- if(!validateIfTagClosed(contentForValidation)){
1611
- response.isBraceError = true;
1612
- response.valid = false;
1613
1522
  }
1614
1523
  return response;
1615
1524
  }
@@ -2843,21 +2752,21 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2843
2752
 
2844
2753
 
2845
2754
  getMissingOrUnsupportedTagsName = (content = '', type) => {
2846
- const { MISSING_TAGS, UNSUPPORTED_TAGS} = tagsTypes;
2755
+ const { MISSING_TAGS } = tagsTypes;
2847
2756
  const tagValidationResponse = this.validateTags(content);
2848
- if (type && (type === MISSING_TAGS || type === UNSUPPORTED_TAGS)) {
2849
- return tagValidationResponse[type].join(', ').toString();
2757
+ if (type && type === MISSING_TAGS) {
2758
+ return (tagValidationResponse[type] || []).join(', ').toString();
2850
2759
  }
2851
2760
  return null;
2852
2761
  };
2853
2762
 
2854
2763
  renderTextAreaContent = (styling, columns, val, isVersionEnable, rows, cols, offset = 0) => {
2855
2764
  const { checkValidation, errorData, currentTab, formData } = this.state;
2856
- const { MISSING_TAGS, UNSUPPORTED_TAGS } = tagsTypes;
2765
+ const { MISSING_TAGS } = tagsTypes;
2857
2766
  const errorType = (isVersionEnable ? errorData[`${currentTab - 1}`][val.id] : errorData[val.id]);
2858
2767
  const ifError = checkValidation && errorType;
2859
2768
  const messageContent = isVersionEnable ? formData[`${currentTab - 1}`][val.id] : formData[val.id];
2860
- const { MISSING_TAG_ERROR, UNSUPPORTED_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2769
+ const { MISSING_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2861
2770
  const { formatMessage } = this.props.intl;
2862
2771
 
2863
2772
  const { accessibleFeatures = [] } = this.props.currentOrgDetails || {};
@@ -2870,9 +2779,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2870
2779
  case MISSING_TAG_ERROR:
2871
2780
  errorMessageText = formatMessage(messages.missingTagsValidationError, {missingTags: this.getMissingOrUnsupportedTagsName(messageContent, MISSING_TAGS)});
2872
2781
  break;
2873
- case UNSUPPORTED_TAG_ERROR:
2874
- errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags: this.getMissingOrUnsupportedTagsName(messageContent, UNSUPPORTED_TAGS)});
2875
- break;
2876
2782
  case TAG_BRACKET_COUNT_MISMATCH_ERROR:
2877
2783
  errorMessageText = formatMessage(globalMessages.unbalanacedCurlyBraces);
2878
2784
  break;
@@ -2883,7 +2789,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2883
2789
  break;
2884
2790
  }
2885
2791
  const prevErrorMessage = this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.[0];
2886
- if (prevErrorMessage !== errorMessageText && errorMessageText && this.liquidFlow()) {
2792
+ if (prevErrorMessage !== errorMessageText && errorMessageText && this.isLiquidFlowSupportedByChannel()) {
2887
2793
  this.setState((prevState) => ({
2888
2794
  liquidErrorMessage: {
2889
2795
  ...prevState.liquidErrorMessage,
@@ -2902,7 +2808,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2902
2808
  id={val.id}
2903
2809
  placeholder={val.placeholder ? val.placeholder : ''}
2904
2810
  className={`${ifError ? 'error-form-builder' : ''}`}
2905
- errorMessage={errorMessageText && !this.liquidFlow() ? errorMessageText : ''}
2811
+ errorMessage={errorMessageText && !this.isLiquidFlowSupportedByChannel() ? errorMessageText : ''}
2906
2812
  label={val.label}
2907
2813
  autosize={val.autosize ? val.autosizeParams : false}
2908
2814
  onChange={(e) => this.updateFormData(e.target.value, val)}
@@ -3985,7 +3891,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3985
3891
  <CapColumn
3986
3892
  style={val.colStyle ? val.colStyle : {border : ""}}
3987
3893
  span={val.width}
3988
- className={`${(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.liquidFlow() && "error-boundary"} `}
3894
+ className={(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.isLiquidFlowSupportedByChannel() ? "error-boundary" : ""}
3989
3895
  >
3990
3896
  <CKEditor
3991
3897
  id={val.id}
@@ -4029,7 +3935,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4029
3935
  isModuleFilterEnabled = this.props.isFullMode;
4030
3936
  }
4031
3937
  columns.push(
4032
- <CapColumn style={val.colStyle ? val.colStyle : {}} span={val.width} className={`${(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.liquidFlow() && "error-boundary"}`}>
3938
+ <CapColumn style={val.colStyle ? val.colStyle : {}} span={val.width} className={(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.isLiquidFlowSupportedByChannel() ? "error-boundary" : ""}>
4033
3939
  <BeeEditor
4034
3940
  uid={uuid}
4035
3941
  tokenData={beeToken}
@@ -4315,7 +4221,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4315
4221
 
4316
4222
 
4317
4223
  return (
4318
- <CapSpin spinning={Boolean((this.liquidFlow() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
4224
+ <CapSpin spinning={Boolean((this.isLiquidFlowSupportedByChannel() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
4319
4225
  <CapRow>
4320
4226
  {this.props.schema && this.renderForm()}
4321
4227
  <SlideBox
@@ -26,10 +26,6 @@ export default defineMessages({
26
26
  id: 'creatives.components.FormBuilder.missingTagsValidationError',
27
27
  defaultMessage: 'Missing tags: {missingTags}. Please add them to this message.',
28
28
  },
29
- unsupportedTagsValidationError: {
30
- id: 'creatives.components.FormBuilder.unsupportedTagsValidationError',
31
- defaultMessage: 'Unsupported tags: {unsupportedTags}. Please remove them from this message.',
32
- },
33
29
  genericTagsValidationError: {
34
30
  id: 'creatives.components.FormBuilder.genericTagsValidationError',
35
31
  defaultMessage: 'Please check the message content for unsupported/missing tags',
@@ -42,10 +38,6 @@ export default defineMessages({
42
38
  id: 'creatives.componentsV2.FormBuilder.missingTags',
43
39
  defaultMessage: 'Missing tags are:',
44
40
  },
45
- unsupportedTags: {
46
- id: 'creatives.componentsV2.FormBuilder.unsupportedTags',
47
- defaultMessage: 'Unsupported tags are:',
48
- },
49
41
  upload: {
50
42
  id: 'creatives.componentsV2.FormBuilder.upload',
51
43
  defaultMessage: 'Upload',
@@ -102,7 +102,6 @@ const HTMLEditor = forwardRef(({
102
102
  onTagSelect = null,
103
103
  onContextChange = null,
104
104
  globalActions = null,
105
- isLiquidEnabled = false, // Controls Liquid tab visibility in ValidationTabs
106
105
  isFullMode = true, // Full mode vs library mode - controls layout and visibility
107
106
  onErrorAcknowledged = null, // Callback when user clicks redirection icon to acknowledge errors
108
107
  onValidationChange = null, // Callback when validation state changes (for parent to track errors)
@@ -573,7 +572,6 @@ const HTMLEditor = forwardRef(({
573
572
  content,
574
573
  layout,
575
574
  validation,
576
- isLiquidEnabled,
577
575
  editorRef: getActiveEditorRef(),
578
576
  handleLabelInsert,
579
577
  handleSave,
@@ -593,7 +591,6 @@ const HTMLEditor = forwardRef(({
593
591
  content,
594
592
  layout,
595
593
  validation,
596
- isLiquidEnabled,
597
594
  getActiveEditorRef,
598
595
  handleLabelInsert,
599
596
  handleSave,
@@ -782,7 +779,6 @@ HTMLEditor.propTypes = {
782
779
  onTagSelect: PropTypes.func,
783
780
  onContextChange: PropTypes.func, // Deprecated: use globalActions instead
784
781
  globalActions: PropTypes.object,
785
- isLiquidEnabled: PropTypes.bool, // Controls Liquid tab visibility in validation
786
782
  isFullMode: PropTypes.bool, // Full mode vs library mode
787
783
  onErrorAcknowledged: PropTypes.func, // Callback when user clicks redirection icon to acknowledge errors
788
784
  onValidationChange: PropTypes.func, // Callback when validation state changes
@@ -816,7 +812,6 @@ HTMLEditor.defaultProps = {
816
812
  onTagSelect: null,
817
813
  onContextChange: null,
818
814
  globalActions: null, // Redux actions for API calls
819
- isLiquidEnabled: false,
820
815
  isFullMode: true, // Default to full mode
821
816
  onErrorAcknowledged: null, // Callback when user clicks redirection icon to acknowledge errors
822
817
  onValidationChange: null, // Callback when validation state changes
@@ -229,7 +229,6 @@ const defaultProps = {
229
229
  channel: 'EMAIL',
230
230
  userLocale: 'en',
231
231
  moduleFilterEnabled: true,
232
- isLiquidEnabled: false,
233
232
  isFullMode: true,
234
233
  onErrorAcknowledged: jest.fn(),
235
234
  onValidationChange: jest.fn(),
@@ -3201,7 +3201,6 @@ describe('HTMLEditor', () => {
3201
3201
  onTagSelect={onTagSelect}
3202
3202
  onContextChange={onContextChange}
3203
3203
  globalActions={globalActions}
3204
- isLiquidEnabled={true}
3205
3204
  isFullMode={false}
3206
3205
  onErrorAcknowledged={onErrorAcknowledged}
3207
3206
  onValidationChange={onValidationChange}
@@ -3261,20 +3260,6 @@ describe('HTMLEditor', () => {
3261
3260
  expect(screen.getByTestId('device-toggle')).toBeInTheDocument();
3262
3261
  });
3263
3262
 
3264
- it('should handle isLiquidEnabled prop', () => {
3265
- render(
3266
- <TestWrapper>
3267
- <HTMLEditor isLiquidEnabled={true} />
3268
- </TestWrapper>
3269
- );
3270
-
3271
- act(() => {
3272
- jest.runAllTimers();
3273
- });
3274
-
3275
- expect(screen.getByTestId('editor-toolbar')).toBeInTheDocument();
3276
- });
3277
-
3278
3263
  it('should handle isFullMode prop', () => {
3279
3264
  render(
3280
3265
  <TestWrapper>
@@ -69,7 +69,7 @@ const CodeEditorPaneComponent = ({
69
69
  }) => {
70
70
  const context = useEditorContext();
71
71
  const {
72
- content, validation, variant, isLiquidEnabled,
72
+ content, validation, variant,
73
73
  } = context || {};
74
74
  const { content: contentValue, updateContent } = content || {};
75
75
  const editorRef = useRef(null);
@@ -298,7 +298,6 @@ const CodeEditorPaneComponent = ({
298
298
  <ValidationErrorDisplay
299
299
  validation={validation}
300
300
  onErrorClick={onErrorClick}
301
- isLiquidEnabled={isLiquidEnabled}
302
301
  className="code-editor-pane__validation"
303
302
  />
304
303
  </div>
@@ -2,11 +2,9 @@ export const expectedStateGetLiquidTagsRequest = {
2
2
  fetchingLiquidTags: true,
3
3
  fetchingSchema: true,
4
4
  fetchingSchemaError: "",
5
- liquidTags: [],
6
5
  messages: [],
7
6
  metaEntities: {
8
7
  layouts: [],
9
- tagLookupMap: {},
10
8
  tags: [],
11
9
  },
12
10
  orgID: "",
@@ -17,11 +15,9 @@ export const expectedStateGetLiquidTagsFailure = {
17
15
  fetchingLiquidTags: false,
18
16
  fetchingSchema: true,
19
17
  fetchingSchemaError: "",
20
- liquidTags: [],
21
18
  messages: [],
22
19
  metaEntities: {
23
20
  layouts: [],
24
- tagLookupMap: {},
25
21
  tags: [],
26
22
  },
27
23
  orgID: "",
@@ -32,11 +28,9 @@ export const expectedStateGetLiquidTagsSuccess = {
32
28
  fetchingLiquidTags: false,
33
29
  fetchingSchema: true,
34
30
  fetchingSchemaError: "",
35
- liquidTags: [],
36
31
  messages: [],
37
32
  metaEntities: {
38
33
  layouts: [],
39
- tagLookupMap: {},
40
34
  tags: [],
41
35
  },
42
36
  orgID: "",
@@ -47,11 +41,9 @@ export const expectedStateGetSchemaForEntitySuccessTAG = {
47
41
  fetchingLiquidTags: false,
48
42
  fetchingSchema: false,
49
43
  fetchingSchemaError: false,
50
- liquidTags: [],
51
44
  messages: [],
52
45
  metaEntities: {
53
46
  layouts: undefined,
54
- tagLookupMap: { undefined: { definition: {} } },
55
47
  tags: { standard: { random: "32" } },
56
48
  },
57
49
  orgID: "",
@@ -62,11 +54,9 @@ export const expectedStateGetSchemaForEntitySuccess = {
62
54
  fetchingLiquidTags: false,
63
55
  fetchingSchema: false,
64
56
  fetchingSchemaError: false,
65
- liquidTags: [],
66
57
  messages: [],
67
58
  metaEntities: {
68
59
  layouts: undefined,
69
- tagLookupMap: undefined,
70
60
  tags: undefined,
71
61
  },
72
62
  orgID: "",
@@ -78,13 +68,9 @@ export const expectedForwardedTags = {
78
68
  fetchingSchema: false,
79
69
  fetchingSchemaError: '',
80
70
  injectedTags: undefined,
81
- liquidTags: [],
82
71
  messages: [],
83
72
  metaEntities: {
84
73
  layouts: [],
85
- tagLookupMap: {
86
-
87
- },
88
74
  tags: [],
89
75
  },
90
76
  orgID: "",