@capillarytech/creatives-library 8.0.288 → 8.0.290-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 (52) 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 +4 -85
  6. package/utils/tagValidations.js +84 -222
  7. package/utils/tests/commonUtil.test.js +461 -118
  8. package/utils/tests/tagValidations.test.js +280 -358
  9. package/v2Components/ErrorInfoNote/index.js +2 -5
  10. package/v2Components/FormBuilder/index.js +78 -161
  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/SlideBoxFooter.js +3 -1
  20. package/v2Containers/CreativesContainer/index.js +19 -6
  21. package/v2Containers/Email/index.js +1 -5
  22. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +10 -62
  23. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +12 -115
  24. package/v2Containers/FTP/index.js +2 -51
  25. package/v2Containers/FTP/messages.js +0 -4
  26. package/v2Containers/InApp/index.js +1 -96
  27. package/v2Containers/InApp/tests/index.test.js +17 -6
  28. package/v2Containers/InappAdvance/index.js +2 -103
  29. package/v2Containers/Line/Container/Text/index.js +0 -1
  30. package/v2Containers/MobilePush/Create/index.js +6 -16
  31. package/v2Containers/MobilePush/Edit/index.js +6 -16
  32. package/v2Containers/MobilePushNew/index.js +2 -33
  33. package/v2Containers/Rcs/index.js +12 -37
  34. package/v2Containers/Sms/Create/index.js +31 -3
  35. package/v2Containers/Sms/Create/messages.js +4 -0
  36. package/v2Containers/Sms/Edit/index.js +29 -3
  37. package/v2Containers/Sms/commonMethods.js +6 -6
  38. package/v2Containers/SmsTrai/Edit/index.js +6 -47
  39. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  40. package/v2Containers/Templates/reducer.js +3 -1
  41. package/v2Containers/Templates/tests/reducer.test.js +12 -0
  42. package/v2Containers/Viber/index.js +0 -1
  43. package/v2Containers/WebPush/Create/components/BrandIconSection.test.js +264 -0
  44. package/v2Containers/WebPush/Create/components/__snapshots__/BrandIconSection.test.js.snap +187 -0
  45. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
  46. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  47. package/v2Containers/WebPush/Create/index.js +2 -2
  48. package/v2Containers/WebPush/Create/preview/tests/NotificationContainer.test.js +269 -0
  49. package/v2Containers/WebPush/Create/utils/validation.js +17 -2
  50. package/v2Containers/WebPush/Create/utils/validation.test.js +0 -24
  51. package/v2Containers/Whatsapp/index.js +9 -17
  52. 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, hasPersonalizationTags, 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() {
@@ -714,7 +712,9 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
714
712
  if (channel && channel.toUpperCase() === SMS) {
715
713
  for (let count = 0; count < this.state.tabCount; count += 1) {
716
714
  if (_.isEmpty(errorData[count])) {
717
- return;
715
+ // Do not return early. An empty tab object can appear transiently and returning here
716
+ // prevents onFormValidityChange from firing, which makes Done appear unresponsive.
717
+ errorData[count] = {};
718
718
  }
719
719
  const index = count + 1;
720
720
  if (!this.state.formData[count]) {
@@ -726,17 +726,19 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
726
726
 
727
727
  let tagValidationResponse = false;
728
728
  if (content) {
729
- tagValidationResponse = this.validateTags(content, tags, injectedTags, false, this.props?.isFullMode);
729
+ tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
730
730
  }
731
-
732
- if (tagValidationResponse.valid) {
731
+
732
+ const tagResult = tagValidationResponse && typeof tagValidationResponse === 'object'
733
+ ? tagValidationResponse
734
+ : { valid: false, missingTags: [], isBraceError: false };
735
+ if (tagResult.valid) {
733
736
  errorData[count][`sms-editor${index > 1 ? index : ''}`] = false;
734
737
  } 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;
738
+ const { MISSING_TAG_ERROR, GENERIC_VALIDATION_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags || {};
739
+ const { missingTags, isBraceError } = tagResult;
740
+ errorData[count][`sms-editor${index > 1 ? index : ''}`] = missingTags && missingTags.length ? MISSING_TAG_ERROR : (isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : GENERIC_VALIDATION_ERROR);
741
+ errorData[count][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
740
742
  isValid = false;
741
743
  }
742
744
  if(content !== '' && (ifUnicode && !unicodeCheck)) {
@@ -764,7 +766,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
764
766
  if (this.state.formData['message-editor'] !== undefined ) {
765
767
  const content = this.state.formData['0']['message-editor'] || '';
766
768
 
767
- const tagValidationResponse = this.validateTags((content), tags, injectedTags, false, this.props?.isFullMode);
769
+ const tagValidationResponse = this.validateTags((content), tags, false, this.props?.isFullMode);
768
770
 
769
771
  if (tagValidationResponse.valid) {
770
772
  errorData = {
@@ -847,7 +849,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
847
849
  errorData[index] = true;
848
850
  isValid = false;
849
851
  } else {
850
- const tagValidationResponse = this.validateTags(content, tags, injectedTags, false, this.props?.isFullMode);
852
+ const tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
851
853
 
852
854
  if (tagValidationResponse.valid) {
853
855
  errorData[index] = false;
@@ -913,14 +915,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
913
915
  isCurrentTabValid = false;
914
916
  } else {
915
917
  errorData[parseInt(index)][`message-editor${selector}`] = false;
916
- const tagValidationResponse = this.validateTags(message, tags, injectedTags, false, this.props?.isFullMode);
918
+ const tagValidationResponse = this.validateTags(message, tags, false, this.props?.isFullMode);
917
919
 
918
920
  if (tagValidationResponse.valid) {
919
921
  errorData[parseInt(index)][`message-editor${selector}`] = false;
920
922
  } else {
921
- const {isBraceError} = tagValidationResponse;
923
+ const { isBraceError } = tagValidationResponse;
922
924
  errorData[parseInt(index)][`message-editor${selector}`] = isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : true;
923
- errorData[parseInt(index)]['invalid-tags'] = tagValidationResponse.unsupportedTags;
924
925
  errorData[parseInt(index)][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
925
926
  isValid = false;
926
927
  }
@@ -946,7 +947,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
946
947
  isCurrentTabValid = false;
947
948
  } else {
948
949
  errorData[parseInt(index)][`message-title${selector}`] = false;
949
- const tagValidationResponse = this.validateTags(title, tags, injectedTags, false, this.props?.isFullMode);
950
+ const tagValidationResponse = this.validateTags(title, tags, false, this.props?.isFullMode);
950
951
 
951
952
  if (tagValidationResponse.valid) {
952
953
  errorData[parseInt(index)][`message-title${selector}`] = false;
@@ -1200,8 +1201,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1200
1201
  if (!content) {
1201
1202
  return false;
1202
1203
  }
1203
- const tagValidationResponse = this.validateTags(content, tags, injectedTags, isEmail, this.props?.isFullMode);
1204
-
1204
+ const tagValidationResponse = this.validateTags(content, tags, isEmail, this.props?.isFullMode);
1205
1205
  // Check for base64 images in email content
1206
1206
  isEmail && containsBase64Images({content, callback:()=>{
1207
1207
  tagValidationResponse.valid = false;
@@ -1217,23 +1217,20 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1217
1217
  errorData[index][currentLang]['template-content'] = true;
1218
1218
  isValid = false;
1219
1219
  isLiquidValid = false;
1220
- if ((showMessages && !isNaN(index)) || this.liquidFlow()) {
1221
- if (tagValidationResponse?.missingTags?.length > 0 || tagValidationResponse?.unsupportedTags?.length > 0) {
1220
+ if ((showMessages && !isNaN(index)) || this.isLiquidFlowSupportedByChannel()) {
1221
+ if (tagValidationResponse?.missingTags?.length > 0) {
1222
1222
  errorString += `${this.props.intl.formatMessage(messages.contentNotValidLanguage)} ${currentLang}\n`;
1223
1223
  }
1224
1224
  if (tagValidationResponse?.missingTags?.length > 0) {
1225
1225
  errorString += `${this.props.intl.formatMessage(messages.missingTags)} ${tagValidationResponse.missingTags.toString()}\n`;
1226
1226
  }
1227
- if (tagValidationResponse?.unsupportedTags?.length > 0) {
1228
- errorString += `${this.props.intl.formatMessage(messages.unsupportedTags)} ${tagValidationResponse.unsupportedTags.toString()}\n`;
1229
- }
1230
1227
  if (tagValidationResponse?.isBraceError){
1231
1228
  errorString += this.props.intl.formatMessage(globalMessages.unbalanacedCurlyBraces);
1232
1229
  }
1233
1230
  if (tagValidationResponse?.isContentEmpty) {
1234
1231
  errorString += this.props.intl.formatMessage(messages.emailBodyEmptyError);
1235
1232
  // Adds a bypass for cases where content is initially empty in the creation flow.
1236
- if(this.liquidFlow()){
1233
+ if(this.isLiquidFlowSupportedByChannel()){
1237
1234
  errorString = "";
1238
1235
  isLiquidValid = true;
1239
1236
  }
@@ -1245,7 +1242,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1245
1242
  }
1246
1243
  }
1247
1244
  if (errorString) {
1248
- if (this.liquidFlow()) {
1245
+ if (this.isLiquidFlowSupportedByChannel()) {
1249
1246
  this.setState(
1250
1247
  (prevState) => ({
1251
1248
  liquidErrorMessage: {
@@ -1272,7 +1269,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1272
1269
  });
1273
1270
  }
1274
1271
 
1275
- const isTemplateValid = this.liquidFlow() ? isLiquidValid : isValid;
1272
+ const isTemplateValid = this.isLiquidFlowSupportedByChannel() ? isLiquidValid : isValid;
1276
1273
  //Updating the state with the error data
1277
1274
  this.setState((prevState) => ({
1278
1275
  isFormValid: isTemplateValid,
@@ -1331,7 +1328,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1331
1328
  }
1332
1329
  onSubmitWrapper = (args) => {
1333
1330
  const {singleTab = null} = args || {};
1334
- if (this.liquidFlow()) {
1331
+ if (this.isLiquidFlowSupportedByChannel()) {
1335
1332
  // For MPUSH, we need to validate both Android and iOS content separately
1336
1333
  if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
1337
1334
  this.validateFormBuilderMPush(this.state.formData, singleTab);
@@ -1375,10 +1372,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1375
1372
  messages,
1376
1373
  onError,
1377
1374
  onSuccess,
1378
- tagLookupMap: this.props?.metaEntities?.tagLookupMap,
1379
- eventContextTags: this.props?.eventContextTags,
1380
- isLiquidFlow: this.liquidFlow(),
1381
- forwardedTags: this.props?.isLoyaltyModule ? this.props?.forwardedTags : {},
1382
1375
  skipTags: this.skipTags.bind(this)
1383
1376
  });
1384
1377
  } else {
@@ -1447,13 +1440,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1447
1440
  getLiquidTags: this.props.actions.getLiquidTags,
1448
1441
  formatMessage: this.props.intl.formatMessage,
1449
1442
  messages: messages,
1450
- tagLookupMap: this.props?.metaEntities?.tagLookupMap,
1451
- eventContextTags: this.props?.eventContextTags,
1452
- isLiquidFlow: this.liquidFlow(), // Use the method instead of props
1453
- forwardedTags: this.props?.isLoyaltyModule ? this.props?.forwardedTags : {},
1454
- skipTags: this.skipTags.bind(this),
1455
- extractNames,
1456
- checkSupport,
1457
1443
  singleTab: singleTab?.toUpperCase(),
1458
1444
  });
1459
1445
  }
@@ -1508,115 +1494,40 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1508
1494
  });
1509
1495
  }
1510
1496
 
1511
- validateTags(content, tagsParam, injectedTagsParams, isEmail = false, isFullMode = this.props?.isFullMode) {
1512
- const type = (this.props.location && this.props.location.query.type) ? this.props.location.query.type : 'full';
1497
+ validateTags(content, tagsParam, isEmail = false, isFullMode = this.props?.isFullMode) {
1513
1498
  let currentModule = this.props.location.query.module ? this.props.location.query.module : 'default';
1514
1499
  if (this.props.tagModule) {
1515
1500
  currentModule = this.props.tagModule;
1516
1501
  }
1517
1502
  const tags = tagsParam ? tagsParam : this.props.tags;
1518
- const injectedTags = this.transformInjectedTags(injectedTagsParams ? injectedTagsParams : this.props.injectedTags);
1519
- const excludedTags = ['user_id_b64', 'outbox_id_b64'];
1520
-
1521
-
1522
- const response = {};
1523
- response.valid = true;
1524
- response.missingTags = [];
1525
- response.unsupportedTags = [];
1526
- response.isBraceError = false;
1527
- response.isContentEmpty = false;
1528
- const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content ;
1529
- const isModuleTypeOutbound = (this.props?.moduleType || '').toUpperCase() === OUTBOUND;
1530
- // Run tag validation (missing + unsupported): library mode, or full mode with liquid support, or
1531
- // legacy Email (CK Editor) when unsubscribe is required (EMAIL_UNSUBSCRIBE_TAG_MANDATORY false) so missing-unsubscribe error shows
1532
- const shouldRunTagValidation = !isFullMode
1533
- || (isEmail && hasLiquidSupportFeature())
1534
- || (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound);
1535
- if (tags && tags.length && !isFullMode) {
1536
- _.forEach(tags, (tag) => {
1537
- _.forEach(tag.definition.supportedModules, (module) => {
1538
- if (module.mandatory && (currentModule === module.context)) {
1539
- if (content.indexOf(`{{${tag.definition.value}}}`) === -1) {
1540
- response.valid = false;
1541
- response.missingTags.push(tag.definition.value);
1542
- }
1543
- }
1544
- });
1545
- });
1546
- // Legacy Email (CK Editor): when unsubscribe is required, ensure we validate it even if tag schema didn't mark it mandatory for this module
1547
- if (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
1548
- const hasUnsubscribeInContent = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g.test(content);
1549
- if (!hasUnsubscribeInContent && response.missingTags.indexOf('unsubscribe') === -1) {
1550
- response.valid = false;
1551
- response.missingTags.push('unsubscribe');
1552
- }
1553
- }
1554
- const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
1555
- let match = regex.exec(content);
1556
- const regexImgSrc=/<img[^>]*\bsrc\s*=\s*"[^"]*{{customer_barcode}}[^"]*"/;
1557
- let matchImg = regexImgSrc.exec(content);
1558
- const regexCustomerBarcode = /{{customer_barcode}}(?![^<]*>)/g;
1559
- let matchCustomerBarcode = regexCustomerBarcode.exec(content);
1560
- // \S matches anything other than a space, a tab, a newline, or a carriage return.
1561
- const validString= /\S/.test(contentForValidation);
1562
- if (isEmailUnsubscribeTagMandatory() && isEmail && isModuleTypeOutbound) {
1563
- const missingTagIndex = response?.missingTags?.indexOf("unsubscribe");
1564
- if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
1565
- response?.missingTags?.splice(missingTagIndex, 1);
1566
- }
1567
- if (validString) {
1568
- response.valid = true;
1569
- } else {
1570
- response.isContentEmpty = true;
1571
- }
1572
- }
1573
- while (match !== null ) {
1574
- const tagValue = match[0].substring(this.indexOfEnd(match[0], '{{'), match[0].indexOf('}}'));
1575
- const tagIndex = match?.index;
1576
- match = regex.exec(content);
1577
- let ifSupported = false;
1578
- _.forEach(tags, (tag) => {
1579
- if (tag.definition.value === tagValue) {
1580
- ifSupported = true;
1581
- }
1582
- if(tagValue === CUSTOMER_BARCODE_TAG && (matchImg === null || matchCustomerBarcode !== null)){
1583
- ifSupported = false;
1584
- }
1585
- });
1586
- const ifSkipped = this.skipTags(tagValue);
1587
- if (ifSkipped) {
1588
- ifSupported = true;
1589
- let isUnsubscribeSkipped = tagValue.indexOf("unsubscribe") != -1 ;
1590
- if (isUnsubscribeSkipped) {
1591
- const missingTagIndex = response.missingTags.indexOf("unsubscribe");
1592
- if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
1593
- response.missingTags.splice(missingTagIndex, 1);
1594
- }
1595
- }
1596
- }
1597
-
1598
- // Event Context Tags support
1599
- this.props?.eventContextTags?.forEach((tag) => {
1600
- if (tagValue === tag?.tagName) {
1601
- ifSupported = true;
1602
- }
1603
- });
1604
-
1605
- ifSupported = ifSupported || this.checkIfSupportedTag(tagValue, injectedTags);
1606
- // Only add to unsupportedTags if not inside a {% ... %} block (scenario 3: liquid orgs also get unsupported-tag errors)
1607
- if (!ifSupported && !isInsideLiquidBlock(content, tagIndex)) {
1608
- response.unsupportedTags.push(tagValue);
1609
- response.valid = false;
1610
- }
1503
+ const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content;
1504
+ const isOutboundModule = (currentModule || '').toUpperCase() === OUTBOUND;
1505
+
1506
+ const initialMissingTags = (tags && tags.length && !isFullMode && isEmail && isOutboundModule && !isEmailUnsubscribeTagOptional() && !hasUnsubscribeTag(content))
1507
+ ? ['unsubscribe']
1508
+ : [];
1509
+
1510
+ const response = validateTagsCore({
1511
+ contentForBraceCheck: contentForValidation,
1512
+ contentForUnsubscribeScan: content,
1513
+ tags,
1514
+ currentModule,
1515
+ isFullMode,
1516
+ initialMissingTags, // [] or ['unsubscribe']; core uses this instead of definition-based when provided
1517
+ skipTagsFn: this.skipTags.bind(this),
1518
+ includeIsContentEmpty: true,
1519
+ });
1611
1520
 
1612
- if (response?.unsupportedTags?.length == 0 && response?.missingTags?.length == 0 ) {
1613
- response.valid = true;
1614
- }
1521
+ // 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.
1522
+ const validString = /\S/.test(contentForValidation);
1523
+ if (isEmailUnsubscribeTagOptional() && isEmail && isOutboundModule) {
1524
+ const missingTagIndex = response.missingTags.indexOf('unsubscribe');
1525
+ if (missingTagIndex !== -1) {
1526
+ response.missingTags.splice(missingTagIndex, 1);
1527
+ }
1528
+ if (!validString) {
1529
+ response.isContentEmpty = true;
1615
1530
  }
1616
- }
1617
- if(!validateIfTagClosed(contentForValidation)){
1618
- response.isBraceError = true;
1619
- response.valid = false;
1620
1531
  }
1621
1532
  return response;
1622
1533
  }
@@ -2850,21 +2761,21 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2850
2761
 
2851
2762
 
2852
2763
  getMissingOrUnsupportedTagsName = (content = '', type) => {
2853
- const { MISSING_TAGS, UNSUPPORTED_TAGS} = tagsTypes;
2764
+ const { MISSING_TAGS } = tagsTypes;
2854
2765
  const tagValidationResponse = this.validateTags(content);
2855
- if (type && (type === MISSING_TAGS || type === UNSUPPORTED_TAGS)) {
2856
- return tagValidationResponse[type].join(', ').toString();
2766
+ if (type && type === MISSING_TAGS) {
2767
+ return (tagValidationResponse[type] || []).join(', ').toString();
2857
2768
  }
2858
2769
  return null;
2859
2770
  };
2860
2771
 
2861
2772
  renderTextAreaContent = (styling, columns, val, isVersionEnable, rows, cols, offset = 0) => {
2862
2773
  const { checkValidation, errorData, currentTab, formData } = this.state;
2863
- const { MISSING_TAGS, UNSUPPORTED_TAGS } = tagsTypes;
2774
+ const { MISSING_TAGS } = tagsTypes;
2864
2775
  const errorType = (isVersionEnable ? errorData[`${currentTab - 1}`][val.id] : errorData[val.id]);
2865
2776
  const ifError = checkValidation && errorType;
2866
2777
  const messageContent = isVersionEnable ? formData[`${currentTab - 1}`][val.id] : formData[val.id];
2867
- const { MISSING_TAG_ERROR, UNSUPPORTED_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2778
+ const { MISSING_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2868
2779
  const { formatMessage } = this.props.intl;
2869
2780
 
2870
2781
  const { accessibleFeatures = [] } = this.props.currentOrgDetails || {};
@@ -2877,9 +2788,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2877
2788
  case MISSING_TAG_ERROR:
2878
2789
  errorMessageText = formatMessage(messages.missingTagsValidationError, {missingTags: this.getMissingOrUnsupportedTagsName(messageContent, MISSING_TAGS)});
2879
2790
  break;
2880
- case UNSUPPORTED_TAG_ERROR:
2881
- errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags: this.getMissingOrUnsupportedTagsName(messageContent, UNSUPPORTED_TAGS)});
2882
- break;
2883
2791
  case TAG_BRACKET_COUNT_MISMATCH_ERROR:
2884
2792
  errorMessageText = formatMessage(globalMessages.unbalanacedCurlyBraces);
2885
2793
  break;
@@ -2894,7 +2802,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2894
2802
  errorMessageText = formatMessage(messages.personalizationTagsErrorMessage);
2895
2803
  }
2896
2804
  const prevErrorMessage = this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.[0];
2897
- if (prevErrorMessage !== errorMessageText && errorMessageText && this.liquidFlow()) {
2805
+ if (prevErrorMessage !== errorMessageText && errorMessageText && this.isLiquidFlowSupportedByChannel()) {
2898
2806
  this.setState((prevState) => ({
2899
2807
  liquidErrorMessage: {
2900
2808
  ...prevState.liquidErrorMessage,
@@ -2905,7 +2813,10 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2905
2813
  this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
2906
2814
  });
2907
2815
  }
2908
- else{
2816
+ // Always render the textarea regardless of the liquid error state update above.
2817
+ // Previously the textarea was inside the `else` branch, so it was not rendered during the
2818
+ // one render cycle when the liquid error message was first set – causing the input to lose
2819
+ // focus whenever a brace/tag error first appeared.
2909
2820
  if (styling === 'semantic') {
2910
2821
  columns.push(
2911
2822
  <CapColumn key="input" span={val.width} offset={offset}>
@@ -2913,7 +2824,14 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2913
2824
  id={val.id}
2914
2825
  placeholder={val.placeholder ? val.placeholder : ''}
2915
2826
  className={`${ifError ? 'error-form-builder' : ''}`}
2916
- errorMessage={errorMessageText && !this.liquidFlow() ? errorMessageText : ''}
2827
+ errorMessage={
2828
+ ifError && errorMessageText && (
2829
+ !this.isLiquidFlowSupportedByChannel() ||
2830
+ [MOBILE_PUSH, INAPP].includes(this.props.schema?.channel?.toUpperCase())
2831
+ )
2832
+ ? errorMessageText
2833
+ : ''
2834
+ }
2917
2835
  label={val.label}
2918
2836
  autosize={val.autosize ? val.autosizeParams : false}
2919
2837
  onChange={(e) => this.updateFormData(e.target.value, val)}
@@ -2944,7 +2862,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2944
2862
  </CapColumn>
2945
2863
  );
2946
2864
  }
2947
- }
2948
2865
  };
2949
2866
 
2950
2867
 
@@ -4007,7 +3924,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4007
3924
  <CapColumn
4008
3925
  style={val.colStyle ? val.colStyle : {border : ""}}
4009
3926
  span={val.width}
4010
- className={`${(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.liquidFlow() && "error-boundary"} `}
3927
+ className={(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.isLiquidFlowSupportedByChannel() ? "error-boundary" : ""}
4011
3928
  >
4012
3929
  <CKEditor
4013
3930
  id={val.id}
@@ -4051,7 +3968,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4051
3968
  isModuleFilterEnabled = this.props.isFullMode;
4052
3969
  }
4053
3970
  columns.push(
4054
- <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"}`}>
3971
+ <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" : ""}>
4055
3972
  <BeeEditor
4056
3973
  uid={uuid}
4057
3974
  tokenData={beeToken}
@@ -4337,7 +4254,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4337
4254
 
4338
4255
 
4339
4256
  return (
4340
- <CapSpin spinning={Boolean((this.liquidFlow() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
4257
+ <CapSpin spinning={Boolean((this.isLiquidFlowSupportedByChannel() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
4341
4258
  <CapRow>
4342
4259
  {this.props.schema && this.renderForm()}
4343
4260
  <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>