@capillarytech/creatives-library 8.0.285-alpha.1 → 8.0.286

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 (45) 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 +83 -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/ErrorInfoNote/index.js +5 -2
  10. package/v2Components/FormBuilder/index.js +158 -64
  11. package/v2Components/FormBuilder/messages.js +8 -0
  12. package/v2Components/HtmlEditor/HTMLEditor.js +5 -0
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  15. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +2 -1
  16. package/v2Containers/Cap/mockData.js +14 -0
  17. package/v2Containers/Cap/reducer.js +55 -3
  18. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  19. package/v2Containers/CreativesContainer/index.js +1 -0
  20. package/v2Containers/Email/index.js +5 -1
  21. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +62 -10
  22. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +115 -12
  23. package/v2Containers/FTP/index.js +51 -2
  24. package/v2Containers/FTP/messages.js +4 -0
  25. package/v2Containers/InApp/index.js +96 -1
  26. package/v2Containers/InApp/tests/index.test.js +6 -17
  27. package/v2Containers/InappAdvance/index.js +103 -2
  28. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +24 -3
  29. package/v2Containers/Line/Container/Text/index.js +1 -0
  30. package/v2Containers/MobilePushNew/index.js +33 -2
  31. package/v2Containers/Rcs/index.js +37 -12
  32. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +18 -4
  33. package/v2Containers/SmsTrai/Create/index.scss +1 -1
  34. package/v2Containers/SmsTrai/Edit/index.js +47 -6
  35. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  36. package/v2Containers/Viber/index.js +1 -0
  37. package/v2Containers/Viber/index.scss +1 -1
  38. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  39. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  40. package/v2Containers/WebPush/Create/index.js +2 -2
  41. package/v2Containers/WebPush/Create/utils/validation.js +9 -18
  42. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -0
  43. package/v2Containers/Whatsapp/index.js +17 -9
  44. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +624 -248
  45. package/v2Containers/Zalo/index.js +11 -3
@@ -186,6 +186,7 @@ export const ErrorInfoNote = (props) => {
186
186
  errorMessages,
187
187
  onErrorClick,
188
188
  onClose,
189
+ isLiquidEnabled = true,
189
190
  intl,
190
191
  useLegacyDisplay = false, // Use simple list display instead of tabs (for BEE Editor)
191
192
  } = props;
@@ -229,7 +230,7 @@ export const ErrorInfoNote = (props) => {
229
230
  const standardErrors = Array.isArray(rawStandardErrors) ? rawStandardErrors : [];
230
231
  const liquidErrors = Array.isArray(rawLiquidErrors) ? rawLiquidErrors : [];
231
232
  const hasStandardErrors = standardErrors.length > 0;
232
- const hasLiquidErrors = liquidErrors.length > 0;
233
+ const hasLiquidErrors = liquidErrors.length > 0 && isLiquidEnabled;
233
234
 
234
235
  if (!hasStandardErrors && !hasLiquidErrors) {
235
236
  return null;
@@ -356,7 +357,7 @@ export const ErrorInfoNote = (props) => {
356
357
  className="error-info-note__tabs"
357
358
  />
358
359
  <CapRow className="error-info-note__actions">
359
- {hasLiquidErrors && (
360
+ {hasLiquidErrors && isLiquidEnabled && (
360
361
  <CapButton
361
362
  type="flat"
362
363
  className="error-info-note__liquid-doc"
@@ -451,6 +452,7 @@ ErrorInfoNote.defaultProps = {
451
452
  },
452
453
  onErrorClick: null,
453
454
  onClose: null,
455
+ isLiquidEnabled: true,
454
456
  intl: null,
455
457
  useLegacyDisplay: false, // Use simple list display for BEE Editor
456
458
  };
@@ -477,6 +479,7 @@ ErrorInfoNote.propTypes = {
477
479
  }),
478
480
  onErrorClick: PropTypes.func,
479
481
  onClose: PropTypes.func,
482
+ isLiquidEnabled: PropTypes.bool,
480
483
  intl: PropTypes.object,
481
484
  useLegacyDisplay: PropTypes.bool, // Use simple list display for BEE Editor
482
485
  };
@@ -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 { preprocessHtml, validateTagsCore, hasUnsubscribeTag } from '../../utils/tagValidations';
53
+ import { checkSupport, extractNames, preprocessHtml, validateIfTagClosed, isInsideLiquidBlock} 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 { isEmailUnsubscribeTagOptional } from '../../utils/common';
63
+ import { hasLiquidSupportFeature, isEmailUnsubscribeTagMandatory } 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,8 +72,10 @@ const {CapRadioGroup} = CapRadio;
72
72
 
73
73
  const tagsTypes = {
74
74
  MISSING_TAGS: 'missingTags',
75
+ UNSUPPORTED_TAGS: 'unsupportedTags',
75
76
  };
76
77
  const errorMessageForTags = {
78
+ UNSUPPORTED_TAG_ERROR: 'unsupportedTagsError',
77
79
  MISSING_TAG_ERROR: 'missingTagsError',
78
80
  GENERIC_VALIDATION_ERROR: 'genericValidationError',
79
81
  TAG_BRACKET_COUNT_MISMATCH_ERROR: 'tagBracketCountMismatchError'
@@ -136,7 +138,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
136
138
  this.handleSetRadioValue = this.handleSetRadioValue.bind(this);
137
139
  this.formElements = [];
138
140
  // Check if the liquid flow feature is supported and the channel is in the supported list.
139
- this.isLiquidFlowSupportedByChannel = this.isLiquidFlowSupportedByChannel.bind(this);
141
+ this.liquidFlow = this.isLiquidFlowSupported.bind(this);
140
142
  this.onSubmitWrapper = this.onSubmitWrapper.bind(this);
141
143
 
142
144
  // Performance optimization: Debounced functions for high-frequency updates
@@ -328,8 +330,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
328
330
  return updatedFormData;
329
331
  }
330
332
 
331
- isLiquidFlowSupportedByChannel = () => {
332
- return Boolean(LIQUID_SUPPORTED_CHANNELS.includes(this.props?.schema?.channel?.toUpperCase()));
333
+ isLiquidFlowSupported = () => {
334
+ return Boolean(LIQUID_SUPPORTED_CHANNELS.includes(this.props?.schema?.channel?.toUpperCase()) && hasLiquidSupportFeature());
333
335
  }
334
336
 
335
337
  componentWillMount() {
@@ -724,19 +726,17 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
724
726
 
725
727
  let tagValidationResponse = false;
726
728
  if (content) {
727
- tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
729
+ tagValidationResponse = this.validateTags(content, tags, injectedTags, false, this.props?.isFullMode);
728
730
  }
729
-
730
- const tagResult = tagValidationResponse && typeof tagValidationResponse === 'object'
731
- ? tagValidationResponse
732
- : { valid: false, missingTags: [], isBraceError: false };
733
- if (tagResult.valid) {
731
+
732
+ if (tagValidationResponse.valid) {
734
733
  errorData[count][`sms-editor${index > 1 ? index : ''}`] = false;
735
734
  } else {
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;
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;
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, false, this.props?.isFullMode);
767
+ const tagValidationResponse = this.validateTags((content), tags, injectedTags, 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, false, this.props?.isFullMode);
850
+ const tagValidationResponse = this.validateTags(content, tags, injectedTags, false, this.props?.isFullMode);
851
851
 
852
852
  if (tagValidationResponse.valid) {
853
853
  errorData[index] = false;
@@ -910,13 +910,14 @@ 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, false, this.props?.isFullMode);
913
+ const tagValidationResponse = this.validateTags(message, tags, injectedTags, 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;
920
921
  errorData[parseInt(index)][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
921
922
  isValid = false;
922
923
  }
@@ -938,7 +939,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
938
939
  isCurrentTabValid = false;
939
940
  } else {
940
941
  errorData[parseInt(index)][`message-title${selector}`] = false;
941
- const tagValidationResponse = this.validateTags(title, tags, false, this.props?.isFullMode);
942
+ const tagValidationResponse = this.validateTags(title, tags, injectedTags, false, this.props?.isFullMode);
942
943
 
943
944
  if (tagValidationResponse.valid) {
944
945
  errorData[parseInt(index)][`message-title${selector}`] = false;
@@ -1192,7 +1193,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1192
1193
  if (!content) {
1193
1194
  return false;
1194
1195
  }
1195
- const tagValidationResponse = this.validateTags(content, tags, isEmail, this.props?.isFullMode);
1196
+ const tagValidationResponse = this.validateTags(content, tags, injectedTags, isEmail, this.props?.isFullMode);
1197
+
1196
1198
  // Check for base64 images in email content
1197
1199
  isEmail && containsBase64Images({content, callback:()=>{
1198
1200
  tagValidationResponse.valid = false;
@@ -1208,20 +1210,23 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1208
1210
  errorData[index][currentLang]['template-content'] = true;
1209
1211
  isValid = false;
1210
1212
  isLiquidValid = false;
1211
- if ((showMessages && !isNaN(index)) || this.isLiquidFlowSupportedByChannel()) {
1212
- if (tagValidationResponse?.missingTags?.length > 0) {
1213
+ if ((showMessages && !isNaN(index)) || this.liquidFlow()) {
1214
+ if (tagValidationResponse?.missingTags?.length > 0 || tagValidationResponse?.unsupportedTags?.length > 0) {
1213
1215
  errorString += `${this.props.intl.formatMessage(messages.contentNotValidLanguage)} ${currentLang}\n`;
1214
1216
  }
1215
1217
  if (tagValidationResponse?.missingTags?.length > 0) {
1216
1218
  errorString += `${this.props.intl.formatMessage(messages.missingTags)} ${tagValidationResponse.missingTags.toString()}\n`;
1217
1219
  }
1220
+ if (tagValidationResponse?.unsupportedTags?.length > 0) {
1221
+ errorString += `${this.props.intl.formatMessage(messages.unsupportedTags)} ${tagValidationResponse.unsupportedTags.toString()}\n`;
1222
+ }
1218
1223
  if (tagValidationResponse?.isBraceError){
1219
1224
  errorString += this.props.intl.formatMessage(globalMessages.unbalanacedCurlyBraces);
1220
1225
  }
1221
1226
  if (tagValidationResponse?.isContentEmpty) {
1222
1227
  errorString += this.props.intl.formatMessage(messages.emailBodyEmptyError);
1223
1228
  // Adds a bypass for cases where content is initially empty in the creation flow.
1224
- if(this.isLiquidFlowSupportedByChannel()){
1229
+ if(this.liquidFlow()){
1225
1230
  errorString = "";
1226
1231
  isLiquidValid = true;
1227
1232
  }
@@ -1233,7 +1238,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1233
1238
  }
1234
1239
  }
1235
1240
  if (errorString) {
1236
- if (this.isLiquidFlowSupportedByChannel()) {
1241
+ if (this.liquidFlow()) {
1237
1242
  this.setState(
1238
1243
  (prevState) => ({
1239
1244
  liquidErrorMessage: {
@@ -1260,7 +1265,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1260
1265
  });
1261
1266
  }
1262
1267
 
1263
- const isTemplateValid = this.isLiquidFlowSupportedByChannel() ? isLiquidValid : isValid;
1268
+ const isTemplateValid = this.liquidFlow() ? isLiquidValid : isValid;
1264
1269
  //Updating the state with the error data
1265
1270
  this.setState((prevState) => ({
1266
1271
  isFormValid: isTemplateValid,
@@ -1319,7 +1324,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1319
1324
  }
1320
1325
  onSubmitWrapper = (args) => {
1321
1326
  const {singleTab = null} = args || {};
1322
- if (this.isLiquidFlowSupportedByChannel()) {
1327
+ if (this.liquidFlow()) {
1323
1328
  // For MPUSH, we need to validate both Android and iOS content separately
1324
1329
  if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
1325
1330
  this.validateFormBuilderMPush(this.state.formData, singleTab);
@@ -1363,6 +1368,10 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1363
1368
  messages,
1364
1369
  onError,
1365
1370
  onSuccess,
1371
+ tagLookupMap: this.props?.metaEntities?.tagLookupMap,
1372
+ eventContextTags: this.props?.eventContextTags,
1373
+ isLiquidFlow: this.liquidFlow(),
1374
+ forwardedTags: this.props?.isLoyaltyModule ? this.props?.forwardedTags : {},
1366
1375
  skipTags: this.skipTags.bind(this)
1367
1376
  });
1368
1377
  } else {
@@ -1431,6 +1440,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1431
1440
  getLiquidTags: this.props.actions.getLiquidTags,
1432
1441
  formatMessage: this.props.intl.formatMessage,
1433
1442
  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,
1434
1450
  singleTab: singleTab?.toUpperCase(),
1435
1451
  });
1436
1452
  }
@@ -1485,41 +1501,116 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1485
1501
  });
1486
1502
  }
1487
1503
 
1488
- validateTags(content, tagsParam, isEmail = false, isFullMode = this.props?.isFullMode) {
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';
1489
1506
  let currentModule = this.props.location.query.module ? this.props.location.query.module : 'default';
1490
1507
  if (this.props.tagModule) {
1491
1508
  currentModule = this.props.tagModule;
1492
1509
  }
1493
1510
  const tags = tagsParam ? tagsParam : this.props.tags;
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
- });
1511
-
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);
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
+ }
1518
1565
  }
1519
- if (!validString) {
1520
- response.isContentEmpty = true;
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
+ }
1604
+
1605
+ if (response?.unsupportedTags?.length == 0 && response?.missingTags?.length == 0 ) {
1606
+ response.valid = true;
1607
+ }
1521
1608
  }
1522
1609
  }
1610
+ if(!validateIfTagClosed(contentForValidation)){
1611
+ response.isBraceError = true;
1612
+ response.valid = false;
1613
+ }
1523
1614
  return response;
1524
1615
  }
1525
1616
  /* eslint-enable */
@@ -2752,21 +2843,21 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2752
2843
 
2753
2844
 
2754
2845
  getMissingOrUnsupportedTagsName = (content = '', type) => {
2755
- const { MISSING_TAGS } = tagsTypes;
2846
+ const { MISSING_TAGS, UNSUPPORTED_TAGS} = tagsTypes;
2756
2847
  const tagValidationResponse = this.validateTags(content);
2757
- if (type && type === MISSING_TAGS) {
2758
- return (tagValidationResponse[type] || []).join(', ').toString();
2848
+ if (type && (type === MISSING_TAGS || type === UNSUPPORTED_TAGS)) {
2849
+ return tagValidationResponse[type].join(', ').toString();
2759
2850
  }
2760
2851
  return null;
2761
2852
  };
2762
2853
 
2763
2854
  renderTextAreaContent = (styling, columns, val, isVersionEnable, rows, cols, offset = 0) => {
2764
2855
  const { checkValidation, errorData, currentTab, formData } = this.state;
2765
- const { MISSING_TAGS } = tagsTypes;
2856
+ const { MISSING_TAGS, UNSUPPORTED_TAGS } = tagsTypes;
2766
2857
  const errorType = (isVersionEnable ? errorData[`${currentTab - 1}`][val.id] : errorData[val.id]);
2767
2858
  const ifError = checkValidation && errorType;
2768
2859
  const messageContent = isVersionEnable ? formData[`${currentTab - 1}`][val.id] : formData[val.id];
2769
- const { MISSING_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2860
+ const { MISSING_TAG_ERROR, UNSUPPORTED_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2770
2861
  const { formatMessage } = this.props.intl;
2771
2862
 
2772
2863
  const { accessibleFeatures = [] } = this.props.currentOrgDetails || {};
@@ -2779,6 +2870,9 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2779
2870
  case MISSING_TAG_ERROR:
2780
2871
  errorMessageText = formatMessage(messages.missingTagsValidationError, {missingTags: this.getMissingOrUnsupportedTagsName(messageContent, MISSING_TAGS)});
2781
2872
  break;
2873
+ case UNSUPPORTED_TAG_ERROR:
2874
+ errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags: this.getMissingOrUnsupportedTagsName(messageContent, UNSUPPORTED_TAGS)});
2875
+ break;
2782
2876
  case TAG_BRACKET_COUNT_MISMATCH_ERROR:
2783
2877
  errorMessageText = formatMessage(globalMessages.unbalanacedCurlyBraces);
2784
2878
  break;
@@ -2789,7 +2883,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2789
2883
  break;
2790
2884
  }
2791
2885
  const prevErrorMessage = this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.[0];
2792
- if (prevErrorMessage !== errorMessageText && errorMessageText && this.isLiquidFlowSupportedByChannel()) {
2886
+ if (prevErrorMessage !== errorMessageText && errorMessageText && this.liquidFlow()) {
2793
2887
  this.setState((prevState) => ({
2794
2888
  liquidErrorMessage: {
2795
2889
  ...prevState.liquidErrorMessage,
@@ -2808,7 +2902,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2808
2902
  id={val.id}
2809
2903
  placeholder={val.placeholder ? val.placeholder : ''}
2810
2904
  className={`${ifError ? 'error-form-builder' : ''}`}
2811
- errorMessage={errorMessageText && !this.isLiquidFlowSupportedByChannel() ? errorMessageText : ''}
2905
+ errorMessage={errorMessageText && !this.liquidFlow() ? errorMessageText : ''}
2812
2906
  label={val.label}
2813
2907
  autosize={val.autosize ? val.autosizeParams : false}
2814
2908
  onChange={(e) => this.updateFormData(e.target.value, val)}
@@ -3891,7 +3985,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3891
3985
  <CapColumn
3892
3986
  style={val.colStyle ? val.colStyle : {border : ""}}
3893
3987
  span={val.width}
3894
- className={(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.isLiquidFlowSupportedByChannel() ? "error-boundary" : ""}
3988
+ className={`${(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.liquidFlow() && "error-boundary"} `}
3895
3989
  >
3896
3990
  <CKEditor
3897
3991
  id={val.id}
@@ -3935,7 +4029,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3935
4029
  isModuleFilterEnabled = this.props.isFullMode;
3936
4030
  }
3937
4031
  columns.push(
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" : ""}>
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"}`}>
3939
4033
  <BeeEditor
3940
4034
  uid={uuid}
3941
4035
  tokenData={beeToken}
@@ -4221,7 +4315,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4221
4315
 
4222
4316
 
4223
4317
  return (
4224
- <CapSpin spinning={Boolean((this.isLiquidFlowSupportedByChannel() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
4318
+ <CapSpin spinning={Boolean((this.liquidFlow() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
4225
4319
  <CapRow>
4226
4320
  {this.props.schema && this.renderForm()}
4227
4321
  <SlideBox
@@ -26,6 +26,10 @@ 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
+ },
29
33
  genericTagsValidationError: {
30
34
  id: 'creatives.components.FormBuilder.genericTagsValidationError',
31
35
  defaultMessage: 'Please check the message content for unsupported/missing tags',
@@ -38,6 +42,10 @@ export default defineMessages({
38
42
  id: 'creatives.componentsV2.FormBuilder.missingTags',
39
43
  defaultMessage: 'Missing tags are:',
40
44
  },
45
+ unsupportedTags: {
46
+ id: 'creatives.componentsV2.FormBuilder.unsupportedTags',
47
+ defaultMessage: 'Unsupported tags are:',
48
+ },
41
49
  upload: {
42
50
  id: 'creatives.componentsV2.FormBuilder.upload',
43
51
  defaultMessage: 'Upload',
@@ -102,6 +102,7 @@ const HTMLEditor = forwardRef(({
102
102
  onTagSelect = null,
103
103
  onContextChange = null,
104
104
  globalActions = null,
105
+ isLiquidEnabled = false, // Controls Liquid tab visibility in ValidationTabs
105
106
  isFullMode = true, // Full mode vs library mode - controls layout and visibility
106
107
  onErrorAcknowledged = null, // Callback when user clicks redirection icon to acknowledge errors
107
108
  onValidationChange = null, // Callback when validation state changes (for parent to track errors)
@@ -572,6 +573,7 @@ const HTMLEditor = forwardRef(({
572
573
  content,
573
574
  layout,
574
575
  validation,
576
+ isLiquidEnabled,
575
577
  editorRef: getActiveEditorRef(),
576
578
  handleLabelInsert,
577
579
  handleSave,
@@ -591,6 +593,7 @@ const HTMLEditor = forwardRef(({
591
593
  content,
592
594
  layout,
593
595
  validation,
596
+ isLiquidEnabled,
594
597
  getActiveEditorRef,
595
598
  handleLabelInsert,
596
599
  handleSave,
@@ -779,6 +782,7 @@ HTMLEditor.propTypes = {
779
782
  onTagSelect: PropTypes.func,
780
783
  onContextChange: PropTypes.func, // Deprecated: use globalActions instead
781
784
  globalActions: PropTypes.object,
785
+ isLiquidEnabled: PropTypes.bool, // Controls Liquid tab visibility in validation
782
786
  isFullMode: PropTypes.bool, // Full mode vs library mode
783
787
  onErrorAcknowledged: PropTypes.func, // Callback when user clicks redirection icon to acknowledge errors
784
788
  onValidationChange: PropTypes.func, // Callback when validation state changes
@@ -812,6 +816,7 @@ HTMLEditor.defaultProps = {
812
816
  onTagSelect: null,
813
817
  onContextChange: null,
814
818
  globalActions: null, // Redux actions for API calls
819
+ isLiquidEnabled: false,
815
820
  isFullMode: true, // Default to full mode
816
821
  onErrorAcknowledged: null, // Callback when user clicks redirection icon to acknowledge errors
817
822
  onValidationChange: null, // Callback when validation state changes
@@ -229,6 +229,7 @@ const defaultProps = {
229
229
  channel: 'EMAIL',
230
230
  userLocale: 'en',
231
231
  moduleFilterEnabled: true,
232
+ isLiquidEnabled: false,
232
233
  isFullMode: true,
233
234
  onErrorAcknowledged: jest.fn(),
234
235
  onValidationChange: jest.fn(),
@@ -3201,6 +3201,7 @@ describe('HTMLEditor', () => {
3201
3201
  onTagSelect={onTagSelect}
3202
3202
  onContextChange={onContextChange}
3203
3203
  globalActions={globalActions}
3204
+ isLiquidEnabled={true}
3204
3205
  isFullMode={false}
3205
3206
  onErrorAcknowledged={onErrorAcknowledged}
3206
3207
  onValidationChange={onValidationChange}
@@ -3260,6 +3261,20 @@ describe('HTMLEditor', () => {
3260
3261
  expect(screen.getByTestId('device-toggle')).toBeInTheDocument();
3261
3262
  });
3262
3263
 
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
+
3263
3278
  it('should handle isFullMode prop', () => {
3264
3279
  render(
3265
3280
  <TestWrapper>
@@ -69,7 +69,7 @@ const CodeEditorPaneComponent = ({
69
69
  }) => {
70
70
  const context = useEditorContext();
71
71
  const {
72
- content, validation, variant,
72
+ content, validation, variant, isLiquidEnabled,
73
73
  } = context || {};
74
74
  const { content: contentValue, updateContent } = content || {};
75
75
  const editorRef = useRef(null);
@@ -298,6 +298,7 @@ const CodeEditorPaneComponent = ({
298
298
  <ValidationErrorDisplay
299
299
  validation={validation}
300
300
  onErrorClick={onErrorClick}
301
+ isLiquidEnabled={isLiquidEnabled}
301
302
  className="code-editor-pane__validation"
302
303
  />
303
304
  </div>