@capillarytech/creatives-library 8.0.290-alpha.4 → 8.0.291

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 +1 -3
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +85 -4
  6. package/utils/tagValidations.js +223 -83
  7. package/utils/tests/commonUtil.test.js +124 -147
  8. package/utils/tests/tagValidations.test.js +358 -441
  9. package/v2Components/ErrorInfoNote/index.js +5 -2
  10. package/v2Components/FormBuilder/index.js +201 -132
  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/SlideBoxContent.js +1 -5
  20. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  21. package/v2Containers/CreativesContainer/index.js +15 -30
  22. package/v2Containers/Email/index.js +5 -1
  23. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
  24. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +137 -29
  25. package/v2Containers/FTP/index.js +51 -2
  26. package/v2Containers/FTP/messages.js +4 -0
  27. package/v2Containers/InApp/index.js +104 -4
  28. package/v2Containers/InApp/tests/index.test.js +6 -17
  29. package/v2Containers/InappAdvance/index.js +108 -4
  30. package/v2Containers/InappAdvance/tests/index.test.js +0 -2
  31. package/v2Containers/Line/Container/Text/index.js +1 -0
  32. package/v2Containers/MobilePush/Create/index.js +19 -42
  33. package/v2Containers/MobilePush/Edit/index.js +19 -42
  34. package/v2Containers/MobilePushNew/index.js +32 -12
  35. package/v2Containers/MobilepushWrapper/index.js +1 -3
  36. package/v2Containers/Rcs/index.js +37 -12
  37. package/v2Containers/Sms/Create/index.js +3 -39
  38. package/v2Containers/Sms/Create/messages.js +0 -4
  39. package/v2Containers/Sms/Edit/index.js +3 -35
  40. package/v2Containers/Sms/commonMethods.js +6 -3
  41. package/v2Containers/SmsTrai/Edit/index.js +47 -11
  42. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  43. package/v2Containers/SmsWrapper/index.js +0 -2
  44. package/v2Containers/Viber/index.js +1 -0
  45. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  46. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  47. package/v2Containers/WebPush/Create/index.js +2 -2
  48. package/v2Containers/WebPush/Create/utils/validation.js +2 -17
  49. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -59
  50. package/v2Containers/Whatsapp/index.js +17 -9
  51. package/v2Containers/Zalo/index.js +11 -3
  52. package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
@@ -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, 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: {
@@ -1269,7 +1272,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1269
1272
  });
1270
1273
  }
1271
1274
 
1272
- const isTemplateValid = this.isLiquidFlowSupportedByChannel() ? isLiquidValid : isValid;
1275
+ const isTemplateValid = this.liquidFlow() ? isLiquidValid : isValid;
1273
1276
  //Updating the state with the error data
1274
1277
  this.setState((prevState) => ({
1275
1278
  isFormValid: isTemplateValid,
@@ -1328,59 +1331,55 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1328
1331
  }
1329
1332
  onSubmitWrapper = (args) => {
1330
1333
  const {singleTab = null} = args || {};
1331
- // Liquid validation (extractTags + Aira) only in library mode
1332
- const runLiquidValidation = this.isLiquidFlowSupportedByChannel() && !this.props.isFullMode;
1333
- if (runLiquidValidation) {
1334
+ if (this.liquidFlow()) {
1334
1335
  // For MPUSH, we need to validate both Android and iOS content separately
1335
1336
  if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
1336
1337
  this.validateFormBuilderMPush(this.state.formData, singleTab);
1337
1338
  return;
1338
1339
  }
1339
-
1340
- // For other channels (EMAIL, SMS, INAPP): only call extractTags if there are no brace/empty errors already.
1341
- // Run sync validation first; if it fails, block and show errors without calling the API.
1342
- this.validateForm(null, null, true, false, () => {
1343
- if (!this.state.isFormValid) {
1344
- this.props.stopValidation();
1345
- return;
1346
- }
1347
- const content = getChannelData(this.props.schema.channel || this.props.channel, this.state.formData, this.props.baseLanguage);
1348
-
1349
- const onError = ({ standardErrors, liquidErrors }) => {
1350
- this.setState(
1351
- (prevState) => ({
1352
- liquidErrorMessage: {
1353
- ...prevState.liquidErrorMessage,
1354
- STANDARD_ERROR_MSG: standardErrors,
1355
- LIQUID_ERROR_MSG: liquidErrors,
1356
- },
1357
- }),
1358
- () => {
1359
- this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage);
1360
- this.props.stopValidation();
1361
- this.props.onFormValidityChange(false, this.state.errorData);
1362
- }
1363
- );
1364
- };
1365
-
1366
- const onSuccess = (contentToSubmit) => {
1367
- const channel = this.props.channel || this.props?.schema?.channel?.toUpperCase();
1368
- if(channel === EMAIL) {
1369
- const content = this.state.formData?.base?.[this.props.baseLanguage]?.["template-content"] || "";
1370
- this.handleLiquidTemplateSubmit(content);
1371
- } else {
1372
- 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();
1373
1357
  }
1374
- };
1375
-
1376
- validateLiquidTemplateContent(content, {
1377
- getLiquidTags: this.props.actions.getLiquidTags,
1378
- formatMessage: this.props.intl.formatMessage,
1379
- messages,
1380
- onError,
1381
- onSuccess,
1382
- skipTags: this.skipTags.bind(this)
1383
- });
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)
1384
1383
  });
1385
1384
  } else {
1386
1385
  this.props.onSubmit(this.state.formData);
@@ -1407,6 +1406,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1407
1406
 
1408
1407
  // Set up callbacks for error and success handling
1409
1408
  const onLiquidError = ({ standardErrors, liquidErrors }) => {
1409
+
1410
1410
  this.setState(
1411
1411
  (prevState) => ({
1412
1412
  liquidErrorMessage: {
@@ -1418,8 +1418,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1418
1418
  () => {
1419
1419
  this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.state.currentTab);
1420
1420
  this.props.stopValidation();
1421
- // Block save: tell parent form is invalid so Done/submit is blocked
1422
- this.props.onFormValidityChange(false, this.state.errorData);
1423
1421
  }
1424
1422
  );
1425
1423
  };
@@ -1449,6 +1447,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1449
1447
  getLiquidTags: this.props.actions.getLiquidTags,
1450
1448
  formatMessage: this.props.intl.formatMessage,
1451
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,
1452
1457
  singleTab: singleTab?.toUpperCase(),
1453
1458
  });
1454
1459
  }
@@ -1503,40 +1508,115 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1503
1508
  });
1504
1509
  }
1505
1510
 
1506
- 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';
1507
1513
  let currentModule = this.props.location.query.module ? this.props.location.query.module : 'default';
1508
1514
  if (this.props.tagModule) {
1509
1515
  currentModule = this.props.tagModule;
1510
1516
  }
1511
1517
  const tags = tagsParam ? tagsParam : this.props.tags;
1512
- const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content;
1513
- const isOutboundModule = (currentModule || '').toUpperCase() === OUTBOUND;
1514
-
1515
- const initialMissingTags = (tags && tags.length && !isFullMode && isEmail && isOutboundModule && !isEmailUnsubscribeTagOptional() && !hasUnsubscribeTag(content))
1516
- ? ['unsubscribe']
1517
- : [];
1518
-
1519
- const response = validateTagsCore({
1520
- contentForBraceCheck: contentForValidation,
1521
- contentForUnsubscribeScan: content,
1522
- tags,
1523
- currentModule,
1524
- isFullMode,
1525
- initialMissingTags, // [] or ['unsubscribe']; core uses this instead of definition-based when provided
1526
- skipTagsFn: this.skipTags.bind(this),
1527
- includeIsContentEmpty: true,
1528
- });
1529
-
1530
- // 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.
1531
- const validString = /\S/.test(contentForValidation);
1532
- if (isEmailUnsubscribeTagOptional() && isEmail && isOutboundModule) {
1533
- const missingTagIndex = response.missingTags.indexOf('unsubscribe');
1534
- if (missingTagIndex !== -1) {
1535
- 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
+ }
1536
1553
  }
1537
- if (!validString) {
1538
- response.isContentEmpty = true;
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
  }
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
+ }
1615
+ }
1616
+ }
1617
+ if(!validateIfTagClosed(contentForValidation)){
1618
+ response.isBraceError = true;
1619
+ response.valid = false;
1540
1620
  }
1541
1621
  return response;
1542
1622
  }
@@ -2770,21 +2850,21 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2770
2850
 
2771
2851
 
2772
2852
  getMissingOrUnsupportedTagsName = (content = '', type) => {
2773
- const { MISSING_TAGS } = tagsTypes;
2853
+ const { MISSING_TAGS, UNSUPPORTED_TAGS} = tagsTypes;
2774
2854
  const tagValidationResponse = this.validateTags(content);
2775
- if (type && type === MISSING_TAGS) {
2776
- return (tagValidationResponse[type] || []).join(', ').toString();
2855
+ if (type && (type === MISSING_TAGS || type === UNSUPPORTED_TAGS)) {
2856
+ return tagValidationResponse[type].join(', ').toString();
2777
2857
  }
2778
2858
  return null;
2779
2859
  };
2780
2860
 
2781
2861
  renderTextAreaContent = (styling, columns, val, isVersionEnable, rows, cols, offset = 0) => {
2782
2862
  const { checkValidation, errorData, currentTab, formData } = this.state;
2783
- const { MISSING_TAGS } = tagsTypes;
2863
+ const { MISSING_TAGS, UNSUPPORTED_TAGS } = tagsTypes;
2784
2864
  const errorType = (isVersionEnable ? errorData[`${currentTab - 1}`][val.id] : errorData[val.id]);
2785
2865
  const ifError = checkValidation && errorType;
2786
2866
  const messageContent = isVersionEnable ? formData[`${currentTab - 1}`][val.id] : formData[val.id];
2787
- const { MISSING_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2867
+ const { MISSING_TAG_ERROR, UNSUPPORTED_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2788
2868
  const { formatMessage } = this.props.intl;
2789
2869
 
2790
2870
  const { accessibleFeatures = [] } = this.props.currentOrgDetails || {};
@@ -2797,6 +2877,9 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2797
2877
  case MISSING_TAG_ERROR:
2798
2878
  errorMessageText = formatMessage(messages.missingTagsValidationError, {missingTags: this.getMissingOrUnsupportedTagsName(messageContent, MISSING_TAGS)});
2799
2879
  break;
2880
+ case UNSUPPORTED_TAG_ERROR:
2881
+ errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags: this.getMissingOrUnsupportedTagsName(messageContent, UNSUPPORTED_TAGS)});
2882
+ break;
2800
2883
  case TAG_BRACKET_COUNT_MISMATCH_ERROR:
2801
2884
  errorMessageText = formatMessage(globalMessages.unbalanacedCurlyBraces);
2802
2885
  break;
@@ -2810,13 +2893,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2810
2893
  if (this.props.restrictPersonalization && hasPersonalizationTags(messageContent)) {
2811
2894
  errorMessageText = formatMessage(messages.personalizationTagsErrorMessage);
2812
2895
  }
2813
- // Empty/required error: only show after user has triggered validation (ifError / "Done").
2814
- // All other errors (brace, personalization, missing tags, generic): show in real time while typing.
2815
- const isContentEmpty = !messageContent || !/\S/.test(String(messageContent).trim());
2816
- const isEmptyError = errorType && isContentEmpty;
2817
- const showError = errorType && (isEmptyError ? ifError : true);
2818
2896
  const prevErrorMessage = this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.[0];
2819
- if (prevErrorMessage !== errorMessageText && errorMessageText && this.isLiquidFlowSupportedByChannel()) {
2897
+ if (prevErrorMessage !== errorMessageText && errorMessageText && this.liquidFlow()) {
2820
2898
  this.setState((prevState) => ({
2821
2899
  liquidErrorMessage: {
2822
2900
  ...prevState.liquidErrorMessage,
@@ -2827,25 +2905,15 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2827
2905
  this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
2828
2906
  });
2829
2907
  }
2830
- // Always render the textarea regardless of the liquid error state update above.
2831
- // Previously the textarea was inside the `else` branch, so it was not rendered during the
2832
- // one render cycle when the liquid error message was first set – causing the input to lose
2833
- // focus whenever a brace/tag error first appeared.
2908
+ else{
2834
2909
  if (styling === 'semantic') {
2835
2910
  columns.push(
2836
2911
  <CapColumn key="input" span={val.width} offset={offset}>
2837
2912
  <TextArea
2838
2913
  id={val.id}
2839
2914
  placeholder={val.placeholder ? val.placeholder : ''}
2840
- className={`${showError ? 'error-form-builder' : ''}`}
2841
- errorMessage={
2842
- showError && errorMessageText && (
2843
- !this.isLiquidFlowSupportedByChannel() ||
2844
- [MOBILE_PUSH, INAPP].includes(this.props.schema?.channel?.toUpperCase())
2845
- )
2846
- ? errorMessageText
2847
- : ''
2848
- }
2915
+ className={`${ifError ? 'error-form-builder' : ''}`}
2916
+ errorMessage={errorMessageText && !this.liquidFlow() ? errorMessageText : ''}
2849
2917
  label={val.label}
2850
2918
  autosize={val.autosize ? val.autosizeParams : false}
2851
2919
  onChange={(e) => this.updateFormData(e.target.value, val)}
@@ -2876,6 +2944,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2876
2944
  </CapColumn>
2877
2945
  );
2878
2946
  }
2947
+ }
2879
2948
  };
2880
2949
 
2881
2950
 
@@ -3938,7 +4007,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3938
4007
  <CapColumn
3939
4008
  style={val.colStyle ? val.colStyle : {border : ""}}
3940
4009
  span={val.width}
3941
- 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"} `}
3942
4011
  >
3943
4012
  <CKEditor
3944
4013
  id={val.id}
@@ -3982,7 +4051,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3982
4051
  isModuleFilterEnabled = this.props.isFullMode;
3983
4052
  }
3984
4053
  columns.push(
3985
- <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"}`}>
3986
4055
  <BeeEditor
3987
4056
  uid={uuid}
3988
4057
  tokenData={beeToken}
@@ -4268,7 +4337,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4268
4337
 
4269
4338
 
4270
4339
  return (
4271
- <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)} >
4272
4341
  <CapRow>
4273
4342
  {this.props.schema && this.renderForm()}
4274
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',