@capillarytech/creatives-library 8.0.309 → 8.0.310

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 (79) hide show
  1. package/constants/unified.js +1 -5
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/services/api.js +0 -17
  5. package/services/tests/api.test.js +0 -85
  6. package/utils/common.js +8 -5
  7. package/utils/commonUtils.js +93 -46
  8. package/utils/tagValidations.js +223 -83
  9. package/utils/tests/commonUtil.test.js +124 -316
  10. package/utils/tests/tagValidations.test.js +358 -441
  11. package/v2Components/CommonTestAndPreview/SendTestMessage.js +49 -78
  12. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +34 -134
  13. package/v2Components/CommonTestAndPreview/actions.js +0 -10
  14. package/v2Components/CommonTestAndPreview/constants.js +1 -15
  15. package/v2Components/CommonTestAndPreview/index.js +19 -80
  16. package/v2Components/CommonTestAndPreview/messages.js +0 -94
  17. package/v2Components/CommonTestAndPreview/reducer.js +0 -10
  18. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +0 -53
  19. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -31
  20. package/v2Components/CommonTestAndPreview/tests/index.test.js +0 -36
  21. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +0 -71
  22. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +0 -377
  23. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +0 -17
  24. package/v2Components/ErrorInfoNote/index.js +5 -2
  25. package/v2Components/FormBuilder/index.js +203 -137
  26. package/v2Components/FormBuilder/messages.js +8 -0
  27. package/v2Components/HtmlEditor/HTMLEditor.js +5 -0
  28. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  29. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  30. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +2 -1
  31. package/v2Containers/Cap/mockData.js +14 -0
  32. package/v2Containers/Cap/reducer.js +55 -3
  33. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  34. package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -5
  35. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  36. package/v2Containers/CreativesContainer/constants.js +0 -6
  37. package/v2Containers/CreativesContainer/index.js +7 -47
  38. package/v2Containers/Email/index.js +5 -1
  39. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
  40. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +120 -20
  41. package/v2Containers/FTP/index.js +51 -2
  42. package/v2Containers/FTP/messages.js +4 -0
  43. package/v2Containers/InApp/index.js +107 -35
  44. package/v2Containers/InApp/tests/index.test.js +6 -17
  45. package/v2Containers/InappAdvance/index.js +112 -4
  46. package/v2Containers/InappAdvance/tests/index.test.js +0 -2
  47. package/v2Containers/Line/Container/Text/index.js +1 -0
  48. package/v2Containers/MobilePush/Create/index.js +19 -59
  49. package/v2Containers/MobilePush/Edit/index.js +20 -48
  50. package/v2Containers/MobilePushNew/index.js +32 -12
  51. package/v2Containers/MobilepushWrapper/index.js +1 -3
  52. package/v2Containers/Rcs/index.js +37 -12
  53. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1276 -1408
  54. package/v2Containers/Sms/Create/index.js +3 -39
  55. package/v2Containers/Sms/Create/messages.js +0 -4
  56. package/v2Containers/Sms/Edit/index.js +3 -35
  57. package/v2Containers/Sms/commonMethods.js +6 -3
  58. package/v2Containers/SmsTrai/Edit/index.js +47 -11
  59. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +294 -327
  60. package/v2Containers/SmsWrapper/index.js +0 -2
  61. package/v2Containers/TemplatesV2/index.js +13 -28
  62. package/v2Containers/Viber/index.js +1 -0
  63. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  64. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  65. package/v2Containers/WebPush/Create/index.js +2 -2
  66. package/v2Containers/WebPush/Create/utils/validation.js +8 -17
  67. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -44
  68. package/v2Containers/Whatsapp/index.js +17 -9
  69. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +4872 -5246
  70. package/v2Containers/Zalo/index.js +11 -3
  71. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +0 -42
  72. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +0 -284
  73. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +0 -72
  74. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +0 -66
  75. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +0 -657
  76. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +0 -172
  77. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +0 -466
  78. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +0 -114
  79. package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
@@ -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, hasPersonalizationTags, 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() {
@@ -712,9 +714,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
712
714
  if (channel && channel.toUpperCase() === SMS) {
713
715
  for (let count = 0; count < this.state.tabCount; count += 1) {
714
716
  if (_.isEmpty(errorData[count])) {
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] = {};
717
+ return;
718
718
  }
719
719
  const index = count + 1;
720
720
  if (!this.state.formData[count]) {
@@ -726,19 +726,17 @@ 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, false, this.props?.isFullMode);
729
+ tagValidationResponse = this.validateTags(content, tags, injectedTags, false, this.props?.isFullMode);
730
730
  }
731
-
732
- const tagResult = tagValidationResponse && typeof tagValidationResponse === 'object'
733
- ? tagValidationResponse
734
- : { valid: false, missingTags: [], isBraceError: false };
735
- if (tagResult.valid) {
731
+
732
+ if (tagValidationResponse.valid) {
736
733
  errorData[count][`sms-editor${index > 1 ? index : ''}`] = false;
737
734
  } else {
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;
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;
742
740
  isValid = false;
743
741
  }
744
742
  if(content !== '' && (ifUnicode && !unicodeCheck)) {
@@ -766,7 +764,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
766
764
  if (this.state.formData['message-editor'] !== undefined ) {
767
765
  const content = this.state.formData['0']['message-editor'] || '';
768
766
 
769
- const tagValidationResponse = this.validateTags((content), tags, false, this.props?.isFullMode);
767
+ const tagValidationResponse = this.validateTags((content), tags, injectedTags, false, this.props?.isFullMode);
770
768
 
771
769
  if (tagValidationResponse.valid) {
772
770
  errorData = {
@@ -849,7 +847,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
849
847
  errorData[index] = true;
850
848
  isValid = false;
851
849
  } else {
852
- const tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
850
+ const tagValidationResponse = this.validateTags(content, tags, injectedTags, false, this.props?.isFullMode);
853
851
 
854
852
  if (tagValidationResponse.valid) {
855
853
  errorData[index] = false;
@@ -915,13 +913,14 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
915
913
  isCurrentTabValid = false;
916
914
  } else {
917
915
  errorData[parseInt(index)][`message-editor${selector}`] = false;
918
- const tagValidationResponse = this.validateTags(message, tags, false, this.props?.isFullMode);
916
+ const tagValidationResponse = this.validateTags(message, tags, injectedTags, false, this.props?.isFullMode);
919
917
 
920
918
  if (tagValidationResponse.valid) {
921
919
  errorData[parseInt(index)][`message-editor${selector}`] = false;
922
920
  } else {
923
- const { isBraceError } = tagValidationResponse;
921
+ const {isBraceError} = tagValidationResponse;
924
922
  errorData[parseInt(index)][`message-editor${selector}`] = isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : true;
923
+ errorData[parseInt(index)]['invalid-tags'] = tagValidationResponse.unsupportedTags;
925
924
  errorData[parseInt(index)][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
926
925
  isValid = false;
927
926
  }
@@ -947,7 +946,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
947
946
  isCurrentTabValid = false;
948
947
  } else {
949
948
  errorData[parseInt(index)][`message-title${selector}`] = false;
950
- const tagValidationResponse = this.validateTags(title, tags, false, this.props?.isFullMode);
949
+ const tagValidationResponse = this.validateTags(title, tags, injectedTags, false, this.props?.isFullMode);
951
950
 
952
951
  if (tagValidationResponse.valid) {
953
952
  errorData[parseInt(index)][`message-title${selector}`] = false;
@@ -1201,7 +1200,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1201
1200
  if (!content) {
1202
1201
  return false;
1203
1202
  }
1204
- const tagValidationResponse = this.validateTags(content, tags, isEmail, this.props?.isFullMode);
1203
+ const tagValidationResponse = this.validateTags(content, tags, injectedTags, isEmail, this.props?.isFullMode);
1204
+
1205
1205
  // Check for base64 images in email content
1206
1206
  isEmail && containsBase64Images({content, callback:()=>{
1207
1207
  tagValidationResponse.valid = false;
@@ -1217,20 +1217,23 @@ 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.isLiquidFlowSupportedByChannel()) {
1221
- if (tagValidationResponse?.missingTags?.length > 0) {
1220
+ if ((showMessages && !isNaN(index)) || this.liquidFlow()) {
1221
+ if (tagValidationResponse?.missingTags?.length > 0 || tagValidationResponse?.unsupportedTags?.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
+ }
1227
1230
  if (tagValidationResponse?.isBraceError){
1228
1231
  errorString += this.props.intl.formatMessage(globalMessages.unbalanacedCurlyBraces);
1229
1232
  }
1230
1233
  if (tagValidationResponse?.isContentEmpty) {
1231
1234
  errorString += this.props.intl.formatMessage(messages.emailBodyEmptyError);
1232
1235
  // Adds a bypass for cases where content is initially empty in the creation flow.
1233
- if(this.isLiquidFlowSupportedByChannel()){
1236
+ if(this.liquidFlow()){
1234
1237
  errorString = "";
1235
1238
  isLiquidValid = true;
1236
1239
  }
@@ -1242,7 +1245,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1242
1245
  }
1243
1246
  }
1244
1247
  if (errorString) {
1245
- if (this.isLiquidFlowSupportedByChannel()) {
1248
+ if (this.liquidFlow()) {
1246
1249
  this.setState(
1247
1250
  (prevState) => ({
1248
1251
  liquidErrorMessage: {
@@ -1254,11 +1257,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1254
1257
  this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
1255
1258
  }
1256
1259
  );
1257
- // Footer shows the error; skip notification for Email CK/BEE (non-HTML) flow to avoid duplicate feedback
1258
- const isEmailChannel = this.props?.schema?.channel?.toUpperCase() === EMAIL;
1259
- if (!isEmailChannel) {
1260
- this.openNotificationWithIcon('error', errorString, 'email-validation-error');
1261
- }
1260
+ // Show toast for liquid flow too so user sees error (scenario 3)
1261
+ this.openNotificationWithIcon('error', errorString, "email-validation-error");
1262
1262
  } else {
1263
1263
  this.openNotificationWithIcon('error', errorString, "email-validation-error");
1264
1264
  }
@@ -1272,7 +1272,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1272
1272
  });
1273
1273
  }
1274
1274
 
1275
- const isTemplateValid = this.isLiquidFlowSupportedByChannel() ? isLiquidValid : isValid;
1275
+ const isTemplateValid = this.liquidFlow() ? isLiquidValid : isValid;
1276
1276
  //Updating the state with the error data
1277
1277
  this.setState((prevState) => ({
1278
1278
  isFormValid: isTemplateValid,
@@ -1331,59 +1331,55 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1331
1331
  }
1332
1332
  onSubmitWrapper = (args) => {
1333
1333
  const {singleTab = null} = args || {};
1334
- // Liquid validation (extractTags + Aira) only in library mode
1335
- const runLiquidValidation = this.isLiquidFlowSupportedByChannel() && !this.props.isFullMode;
1336
- if (runLiquidValidation) {
1334
+ if (this.liquidFlow() && !this.props?.isFullMode) {
1337
1335
  // For MPUSH, we need to validate both Android and iOS content separately
1338
1336
  if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
1339
1337
  this.validateFormBuilderMPush(this.state.formData, singleTab);
1340
1338
  return;
1341
1339
  }
1342
-
1343
- // For other channels (EMAIL, SMS, INAPP): only call extractTags if there are no brace/empty errors already.
1344
- // Run sync validation first; if it fails, block and show errors without calling the API.
1345
- this.validateForm(null, null, true, false, () => {
1346
- if (!this.state.isFormValid) {
1347
- this.props.stopValidation();
1348
- return;
1349
- }
1350
- const content = getChannelData(this.props.schema.channel || this.props.channel, this.state.formData, this.props.baseLanguage);
1351
-
1352
- const onError = ({ standardErrors, liquidErrors }) => {
1353
- this.setState(
1354
- (prevState) => ({
1355
- liquidErrorMessage: {
1356
- ...prevState.liquidErrorMessage,
1357
- STANDARD_ERROR_MSG: standardErrors,
1358
- LIQUID_ERROR_MSG: liquidErrors,
1359
- },
1360
- }),
1361
- () => {
1362
- this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage);
1363
- this.props.stopValidation();
1364
- this.props.onFormValidityChange(false, this.state.errorData);
1365
- }
1366
- );
1367
- };
1368
-
1369
- const onSuccess = (contentToSubmit) => {
1370
- const channel = this.props.channel || this.props?.schema?.channel?.toUpperCase();
1371
- if(channel === EMAIL) {
1372
- const content = this.state.formData?.base?.[this.props.baseLanguage]?.["template-content"] || "";
1373
- this.handleLiquidTemplateSubmit(content);
1374
- } else {
1375
- this.handleLiquidTemplateSubmit(contentToSubmit);
1340
+
1341
+ // For other channels (EMAIL, SMS, INAPP)
1342
+ const content = getChannelData(this.props.schema.channel || this.props.channel, this.state.formData, this.props.baseLanguage);
1343
+
1344
+ // Set up callbacks for error and success handling
1345
+ const onError = ({ standardErrors, liquidErrors }) => {
1346
+ this.setState(
1347
+ (prevState) => ({
1348
+ liquidErrorMessage: {
1349
+ ...prevState.liquidErrorMessage,
1350
+ STANDARD_ERROR_MSG: standardErrors,
1351
+ LIQUID_ERROR_MSG: liquidErrors,
1352
+ },
1353
+ }),
1354
+ () => {
1355
+ this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage);
1356
+ this.props.stopValidation();
1376
1357
  }
1377
- };
1378
-
1379
- validateLiquidTemplateContent(content, {
1380
- getLiquidTags: this.props.actions.getLiquidTags,
1381
- formatMessage: this.props.intl.formatMessage,
1382
- messages,
1383
- onError,
1384
- onSuccess,
1385
- skipTags: this.skipTags.bind(this)
1386
- });
1358
+ );
1359
+ };
1360
+
1361
+ const onSuccess = (contentToSubmit) => {
1362
+ const channel = this.props.channel || this.props?.schema?.channel?.toUpperCase();
1363
+ if(channel === EMAIL) {
1364
+ const content = this.state.formData?.base?.[this.props.baseLanguage]?.["template-content"] || "";
1365
+ this.handleLiquidTemplateSubmit(content);
1366
+ } else {
1367
+ this.handleLiquidTemplateSubmit(contentToSubmit);
1368
+ }
1369
+ };
1370
+
1371
+ // Call the common validation function
1372
+ validateLiquidTemplateContent(content, {
1373
+ getLiquidTags: this.props.actions.getLiquidTags,
1374
+ formatMessage: this.props.intl.formatMessage,
1375
+ messages,
1376
+ onError,
1377
+ 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
+ skipTags: this.skipTags.bind(this)
1387
1383
  });
1388
1384
  } else {
1389
1385
  this.props.onSubmit(this.state.formData);
@@ -1410,6 +1406,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1410
1406
 
1411
1407
  // Set up callbacks for error and success handling
1412
1408
  const onLiquidError = ({ standardErrors, liquidErrors }) => {
1409
+
1413
1410
  this.setState(
1414
1411
  (prevState) => ({
1415
1412
  liquidErrorMessage: {
@@ -1421,8 +1418,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1421
1418
  () => {
1422
1419
  this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.state.currentTab);
1423
1420
  this.props.stopValidation();
1424
- // Block save: tell parent form is invalid so Done/submit is blocked
1425
- this.props.onFormValidityChange(false, this.state.errorData);
1426
1421
  }
1427
1422
  );
1428
1423
  };
@@ -1452,6 +1447,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1452
1447
  getLiquidTags: this.props.actions.getLiquidTags,
1453
1448
  formatMessage: this.props.intl.formatMessage,
1454
1449
  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,
1455
1457
  singleTab: singleTab?.toUpperCase(),
1456
1458
  });
1457
1459
  }
@@ -1506,41 +1508,116 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1506
1508
  });
1507
1509
  }
1508
1510
 
1509
- validateTags(content, tagsParam, isEmail = false, isFullMode = this.props?.isFullMode) {
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';
1510
1513
  let currentModule = this.props.location.query.module ? this.props.location.query.module : 'default';
1511
1514
  if (this.props.tagModule) {
1512
1515
  currentModule = this.props.tagModule;
1513
1516
  }
1514
1517
  const tags = tagsParam ? tagsParam : this.props.tags;
1515
- const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content;
1516
- const isOutboundModule = (currentModule || '').toUpperCase() === OUTBOUND;
1517
-
1518
- const initialMissingTags = (tags && tags.length && !isFullMode && isEmail && isOutboundModule && !isEmailUnsubscribeTagOptional() && !hasUnsubscribeTag(content))
1519
- ? ['unsubscribe']
1520
- : [];
1521
-
1522
- const response = validateTagsCore({
1523
- contentForBraceCheck: contentForValidation,
1524
- contentForUnsubscribeScan: content,
1525
- tags,
1526
- currentModule,
1527
- isFullMode,
1528
- initialMissingTags, // [] or ['unsubscribe']; core uses this instead of definition-based when provided
1529
- skipTagsFn: this.skipTags.bind(this),
1530
- includeIsContentEmpty: true,
1531
- });
1532
-
1533
- // 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.
1534
- const validString = /\S/.test(contentForValidation);
1535
- if (isEmailUnsubscribeTagOptional() && isEmail && isOutboundModule) {
1536
- const missingTagIndex = response.missingTags.indexOf('unsubscribe');
1537
- if (missingTagIndex !== -1) {
1538
- response.missingTags.splice(missingTagIndex, 1);
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
+ }
1539
1572
  }
1540
- if (!validString) {
1541
- response.isContentEmpty = true;
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
+ }
1611
+
1612
+ if (response?.unsupportedTags?.length == 0 && response?.missingTags?.length == 0 ) {
1613
+ response.valid = true;
1614
+ }
1542
1615
  }
1543
1616
  }
1617
+ if(!validateIfTagClosed(contentForValidation)){
1618
+ response.isBraceError = true;
1619
+ response.valid = false;
1620
+ }
1544
1621
  return response;
1545
1622
  }
1546
1623
  /* eslint-enable */
@@ -2773,21 +2850,21 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2773
2850
 
2774
2851
 
2775
2852
  getMissingOrUnsupportedTagsName = (content = '', type) => {
2776
- const { MISSING_TAGS } = tagsTypes;
2853
+ const { MISSING_TAGS, UNSUPPORTED_TAGS} = tagsTypes;
2777
2854
  const tagValidationResponse = this.validateTags(content);
2778
- if (type && type === MISSING_TAGS) {
2779
- return (tagValidationResponse[type] || []).join(', ').toString();
2855
+ if (type && (type === MISSING_TAGS || type === UNSUPPORTED_TAGS)) {
2856
+ return tagValidationResponse[type].join(', ').toString();
2780
2857
  }
2781
2858
  return null;
2782
2859
  };
2783
2860
 
2784
2861
  renderTextAreaContent = (styling, columns, val, isVersionEnable, rows, cols, offset = 0) => {
2785
2862
  const { checkValidation, errorData, currentTab, formData } = this.state;
2786
- const { MISSING_TAGS } = tagsTypes;
2863
+ const { MISSING_TAGS, UNSUPPORTED_TAGS } = tagsTypes;
2787
2864
  const errorType = (isVersionEnable ? errorData[`${currentTab - 1}`][val.id] : errorData[val.id]);
2788
2865
  const ifError = checkValidation && errorType;
2789
2866
  const messageContent = isVersionEnable ? formData[`${currentTab - 1}`][val.id] : formData[val.id];
2790
- const { MISSING_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2867
+ const { MISSING_TAG_ERROR, UNSUPPORTED_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2791
2868
  const { formatMessage } = this.props.intl;
2792
2869
 
2793
2870
  const { accessibleFeatures = [] } = this.props.currentOrgDetails || {};
@@ -2800,6 +2877,9 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2800
2877
  case MISSING_TAG_ERROR:
2801
2878
  errorMessageText = formatMessage(messages.missingTagsValidationError, {missingTags: this.getMissingOrUnsupportedTagsName(messageContent, MISSING_TAGS)});
2802
2879
  break;
2880
+ case UNSUPPORTED_TAG_ERROR:
2881
+ errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags: this.getMissingOrUnsupportedTagsName(messageContent, UNSUPPORTED_TAGS)});
2882
+ break;
2803
2883
  case TAG_BRACKET_COUNT_MISMATCH_ERROR:
2804
2884
  errorMessageText = formatMessage(globalMessages.unbalanacedCurlyBraces);
2805
2885
  break;
@@ -2813,13 +2893,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2813
2893
  if (this.props.restrictPersonalization && hasPersonalizationTags(messageContent)) {
2814
2894
  errorMessageText = formatMessage(messages.personalizationTagsErrorMessage);
2815
2895
  }
2816
- // Empty/required error: only show after user has triggered validation (ifError / "Done").
2817
- // All other errors (brace, personalization, missing tags, generic): show in real time while typing.
2818
- const isContentEmpty = !messageContent || !/\S/.test(String(messageContent).trim());
2819
- const isEmptyError = errorType && isContentEmpty;
2820
- const showError = errorType && (isEmptyError ? ifError : true);
2821
2896
  const prevErrorMessage = this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.[0];
2822
- if (prevErrorMessage !== errorMessageText && errorMessageText && this.isLiquidFlowSupportedByChannel()) {
2897
+ if (prevErrorMessage !== errorMessageText && errorMessageText && this.liquidFlow()) {
2823
2898
  this.setState((prevState) => ({
2824
2899
  liquidErrorMessage: {
2825
2900
  ...prevState.liquidErrorMessage,
@@ -2830,25 +2905,15 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2830
2905
  this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
2831
2906
  });
2832
2907
  }
2833
- // Always render the textarea regardless of the liquid error state update above.
2834
- // Previously the textarea was inside the `else` branch, so it was not rendered during the
2835
- // one render cycle when the liquid error message was first set – causing the input to lose
2836
- // focus whenever a brace/tag error first appeared.
2908
+ else{
2837
2909
  if (styling === 'semantic') {
2838
2910
  columns.push(
2839
2911
  <CapColumn key="input" span={val.width} offset={offset}>
2840
2912
  <TextArea
2841
2913
  id={val.id}
2842
2914
  placeholder={val.placeholder ? val.placeholder : ''}
2843
- className={`${showError ? 'error-form-builder' : ''}`}
2844
- errorMessage={
2845
- showError && errorMessageText && (
2846
- !this.isLiquidFlowSupportedByChannel() ||
2847
- [MOBILE_PUSH, INAPP].includes(this.props.schema?.channel?.toUpperCase())
2848
- )
2849
- ? errorMessageText
2850
- : ''
2851
- }
2915
+ className={`${ifError ? 'error-form-builder' : ''}`}
2916
+ errorMessage={errorMessageText && !this.liquidFlow() ? errorMessageText : ''}
2852
2917
  label={val.label}
2853
2918
  autosize={val.autosize ? val.autosizeParams : false}
2854
2919
  onChange={(e) => this.updateFormData(e.target.value, val)}
@@ -2879,6 +2944,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2879
2944
  </CapColumn>
2880
2945
  );
2881
2946
  }
2947
+ }
2882
2948
  };
2883
2949
 
2884
2950
 
@@ -3941,7 +4007,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3941
4007
  <CapColumn
3942
4008
  style={val.colStyle ? val.colStyle : {border : ""}}
3943
4009
  span={val.width}
3944
- className={(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.isLiquidFlowSupportedByChannel() ? "error-boundary" : ""}
4010
+ className={`${(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.liquidFlow() && "error-boundary"} `}
3945
4011
  >
3946
4012
  <CKEditor
3947
4013
  id={val.id}
@@ -3985,7 +4051,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3985
4051
  isModuleFilterEnabled = this.props.isFullMode;
3986
4052
  }
3987
4053
  columns.push(
3988
- <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" : ""}>
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"}`}>
3989
4055
  <BeeEditor
3990
4056
  uid={uuid}
3991
4057
  tokenData={beeToken}
@@ -4271,7 +4337,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4271
4337
 
4272
4338
 
4273
4339
  return (
4274
- <CapSpin spinning={Boolean((this.isLiquidFlowSupportedByChannel() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
4340
+ <CapSpin spinning={Boolean((this.liquidFlow() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
4275
4341
  <CapRow>
4276
4342
  {this.props.schema && this.renderForm()}
4277
4343
  <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)
@@ -573,6 +574,7 @@ const HTMLEditor = forwardRef(({
573
574
  content,
574
575
  layout,
575
576
  validation,
577
+ isLiquidEnabled,
576
578
  editorRef: getActiveEditorRef(),
577
579
  handleLabelInsert,
578
580
  handleSave,
@@ -592,6 +594,7 @@ const HTMLEditor = forwardRef(({
592
594
  content,
593
595
  layout,
594
596
  validation,
597
+ isLiquidEnabled,
595
598
  getActiveEditorRef,
596
599
  handleLabelInsert,
597
600
  handleSave,
@@ -780,6 +783,7 @@ HTMLEditor.propTypes = {
780
783
  onTagSelect: PropTypes.func,
781
784
  onContextChange: PropTypes.func, // Deprecated: use globalActions instead
782
785
  globalActions: PropTypes.object,
786
+ isLiquidEnabled: PropTypes.bool, // Controls Liquid tab visibility in validation
783
787
  isFullMode: PropTypes.bool, // Full mode vs library mode
784
788
  onErrorAcknowledged: PropTypes.func, // Callback when user clicks redirection icon to acknowledge errors
785
789
  onValidationChange: PropTypes.func, // Callback when validation state changes
@@ -813,6 +817,7 @@ HTMLEditor.defaultProps = {
813
817
  onTagSelect: null,
814
818
  onContextChange: null,
815
819
  globalActions: null, // Redux actions for API calls
820
+ isLiquidEnabled: false,
816
821
  isFullMode: true, // Default to full mode
817
822
  onErrorAcknowledged: null, // Callback when user clicks redirection icon to acknowledge errors
818
823
  onValidationChange: null, // Callback when validation state changes