@capillarytech/creatives-library 8.0.287-alpha.3 → 8.0.288

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 (65) hide show
  1. package/constants/unified.js +1 -0
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +111 -2
  6. package/utils/tagValidations.js +222 -84
  7. package/utils/tests/commonUtil.test.js +118 -147
  8. package/utils/tests/tagValidations.test.js +358 -280
  9. package/v2Components/CapTagList/index.js +7 -2
  10. package/v2Components/CapTagListWithInput/index.js +4 -0
  11. package/v2Components/ErrorInfoNote/index.js +5 -2
  12. package/v2Components/FormBuilder/index.js +187 -74
  13. package/v2Components/FormBuilder/messages.js +12 -0
  14. package/v2Components/HtmlEditor/HTMLEditor.js +5 -0
  15. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  16. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  17. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +2 -1
  18. package/v2Containers/Cap/mockData.js +14 -0
  19. package/v2Containers/Cap/reducer.js +55 -3
  20. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  21. package/v2Containers/CreativesContainer/SlideBoxContent.js +20 -0
  22. package/v2Containers/CreativesContainer/SlideBoxFooter.js +40 -6
  23. package/v2Containers/CreativesContainer/constants.js +6 -0
  24. package/v2Containers/CreativesContainer/index.js +36 -5
  25. package/v2Containers/CreativesContainer/messages.js +12 -0
  26. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +339 -0
  27. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +18 -0
  28. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +37 -0
  29. package/v2Containers/Email/index.js +5 -1
  30. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +62 -10
  31. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +115 -12
  32. package/v2Containers/FTP/index.js +51 -2
  33. package/v2Containers/FTP/messages.js +4 -0
  34. package/v2Containers/InApp/index.js +96 -1
  35. package/v2Containers/InApp/tests/index.test.js +6 -17
  36. package/v2Containers/InappAdvance/index.js +103 -2
  37. package/v2Containers/Line/Container/Text/index.js +1 -0
  38. package/v2Containers/MobilePush/Create/index.js +37 -1
  39. package/v2Containers/MobilePush/Create/messages.js +4 -0
  40. package/v2Containers/MobilePush/Edit/index.js +37 -2
  41. package/v2Containers/MobilePush/Edit/messages.js +4 -0
  42. package/v2Containers/MobilePushNew/components/PlatformContentFields.js +36 -12
  43. package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +68 -27
  44. package/v2Containers/MobilePushNew/index.js +92 -5
  45. package/v2Containers/MobilePushNew/messages.js +8 -0
  46. package/v2Containers/MobilepushWrapper/index.js +7 -1
  47. package/v2Containers/Rcs/index.js +37 -12
  48. package/v2Containers/Sms/Create/index.js +3 -31
  49. package/v2Containers/Sms/Create/messages.js +0 -4
  50. package/v2Containers/Sms/Edit/index.js +3 -29
  51. package/v2Containers/Sms/commonMethods.js +6 -6
  52. package/v2Containers/SmsTrai/Edit/index.js +47 -6
  53. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  54. package/v2Containers/TagList/index.js +17 -1
  55. package/v2Containers/TagList/messages.js +4 -0
  56. package/v2Containers/TemplatesV2/index.js +43 -23
  57. package/v2Containers/Viber/index.js +1 -0
  58. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  59. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  60. package/v2Containers/WebPush/Create/index.js +25 -6
  61. package/v2Containers/WebPush/Create/messages.js +8 -1
  62. package/v2Containers/WebPush/Create/utils/validation.js +20 -22
  63. package/v2Containers/WebPush/Create/utils/validation.test.js +52 -0
  64. package/v2Containers/Whatsapp/index.js +17 -9
  65. package/v2Containers/Zalo/index.js +11 -3
@@ -456,6 +456,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
456
456
  </CapSpin>
457
457
  </CapRow>
458
458
  );
459
+ const {disableTooltipMsg, disabled} = this.props || {};
459
460
  return (
460
461
  <>
461
462
  {hidePopover ? (
@@ -476,7 +477,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
476
477
  >
477
478
  <CapTooltip
478
479
  title={
479
- fetchingSchemaError && (
480
+ disableTooltipMsg && disabled ? (
481
+ disableTooltipMsg
482
+ ) : fetchingSchemaError ? (
480
483
  <CapRow className="tooltip-text-container">
481
484
  <CapLabel className="tooltip-text1">
482
485
  {formatMessage(messages.somethingWentWrong)}
@@ -485,7 +488,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
485
488
  {formatMessage(messages.labelFetchErrorMsg)}
486
489
  </CapLabel>
487
490
  </CapRow>
488
- )
491
+ ) : null
489
492
  }
490
493
  placement="right"
491
494
  >
@@ -538,6 +541,8 @@ CapTagList.propTypes = {
538
541
  currentOrgDetails: PropTypes.object,
539
542
  channel: PropTypes.string,
540
543
  disabled: PropTypes.bool,
544
+ // Optional custom tooltip message when disabled (used for personalization restriction)
545
+ disableTooltipMsg: PropTypes.string,
541
546
  fetchingSchemaError: PropTypes.bool,
542
547
  popoverPlacement: PropTypes.string,
543
548
  };
@@ -26,6 +26,7 @@ export const CapTagListWithInput = (props) => {
26
26
  className = '',
27
27
  userLocale = 'en',
28
28
  eventContextTags = [],
29
+ restrictPersonalization = false,
29
30
  // CapInput props
30
31
  inputId,
31
32
  inputValue = '',
@@ -78,6 +79,7 @@ export const CapTagListWithInput = (props) => {
78
79
  eventContextTags={eventContextTags}
79
80
  style={tagListStyle}
80
81
  popoverPlacement={popoverPlacement}
82
+ restrictPersonalization={restrictPersonalization}
81
83
  />
82
84
  )}
83
85
  </CapRow>
@@ -113,6 +115,7 @@ CapTagListWithInput.propTypes = {
113
115
  className: PropTypes.string,
114
116
  userLocale: PropTypes.string,
115
117
  eventContextTags: PropTypes.array,
118
+ restrictPersonalization: PropTypes.bool,
116
119
 
117
120
  // CapInput props
118
121
  inputId: PropTypes.string.isRequired,
@@ -150,6 +153,7 @@ CapTagListWithInput.defaultProps = {
150
153
  className: '',
151
154
  userLocale: 'en',
152
155
  eventContextTags: [],
156
+ restrictPersonalization: false,
153
157
  inputValue: '',
154
158
  inputSize: 'default',
155
159
  inputRequired: false,
@@ -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,10 +60,10 @@ 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
- import { getChannelData, validateLiquidTemplateContent, validateMobilePushContent } from '../../utils/commonUtils';
66
+ import { getChannelData, hasPersonalizationTags, validateLiquidTemplateContent, validateMobilePushContent } from '../../utils/commonUtils';
67
67
  const TabPane = Tabs.TabPane;
68
68
  const {Column} = Table;
69
69
  const {TextArea} = CapInput;
@@ -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;
@@ -905,20 +903,24 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
905
903
  if (errorData[parseInt(index)]) {
906
904
  if (message) {
907
905
  message = message.trim();
908
-
909
906
  if (message === "") {
910
907
  errorData[parseInt(index)][`message-editor${selector}`] = true;
911
908
  isValid = false;
912
909
  isCurrentTabValid = false;
910
+ } else if (this.props.restrictPersonalization && hasPersonalizationTags(message)) {
911
+ errorData[parseInt(index)][`message-editor${selector}`] = true;
912
+ isValid = false;
913
+ isCurrentTabValid = false;
913
914
  } else {
914
915
  errorData[parseInt(index)][`message-editor${selector}`] = false;
915
- const tagValidationResponse = this.validateTags(message, tags, false, this.props?.isFullMode);
916
+ const tagValidationResponse = this.validateTags(message, tags, injectedTags, false, this.props?.isFullMode);
916
917
 
917
918
  if (tagValidationResponse.valid) {
918
919
  errorData[parseInt(index)][`message-editor${selector}`] = false;
919
920
  } else {
920
- const { isBraceError } = tagValidationResponse;
921
+ const {isBraceError} = tagValidationResponse;
921
922
  errorData[parseInt(index)][`message-editor${selector}`] = isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : true;
923
+ errorData[parseInt(index)]['invalid-tags'] = tagValidationResponse.unsupportedTags;
922
924
  errorData[parseInt(index)][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
923
925
  isValid = false;
924
926
  }
@@ -938,9 +940,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
938
940
  errorData[parseInt(index)][`message-title${selector}`] = true;
939
941
  isValid = false;
940
942
  isCurrentTabValid = false;
943
+ } else if (this.props.restrictPersonalization && hasPersonalizationTags(title)) {
944
+ errorData[parseInt(index)][`message-title${selector}`] = true;
945
+ isValid = false;
946
+ isCurrentTabValid = false;
941
947
  } else {
942
948
  errorData[parseInt(index)][`message-title${selector}`] = false;
943
- const tagValidationResponse = this.validateTags(title, tags, false, this.props?.isFullMode);
949
+ const tagValidationResponse = this.validateTags(title, tags, injectedTags, false, this.props?.isFullMode);
944
950
 
945
951
  if (tagValidationResponse.valid) {
946
952
  errorData[parseInt(index)][`message-title${selector}`] = false;
@@ -1194,7 +1200,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1194
1200
  if (!content) {
1195
1201
  return false;
1196
1202
  }
1197
- const tagValidationResponse = this.validateTags(content, tags, isEmail, this.props?.isFullMode);
1203
+ const tagValidationResponse = this.validateTags(content, tags, injectedTags, isEmail, this.props?.isFullMode);
1204
+
1198
1205
  // Check for base64 images in email content
1199
1206
  isEmail && containsBase64Images({content, callback:()=>{
1200
1207
  tagValidationResponse.valid = false;
@@ -1210,20 +1217,23 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1210
1217
  errorData[index][currentLang]['template-content'] = true;
1211
1218
  isValid = false;
1212
1219
  isLiquidValid = false;
1213
- if ((showMessages && !isNaN(index)) || this.isLiquidFlowSupportedByChannel()) {
1214
- if (tagValidationResponse?.missingTags?.length > 0) {
1220
+ if ((showMessages && !isNaN(index)) || this.liquidFlow()) {
1221
+ if (tagValidationResponse?.missingTags?.length > 0 || tagValidationResponse?.unsupportedTags?.length > 0) {
1215
1222
  errorString += `${this.props.intl.formatMessage(messages.contentNotValidLanguage)} ${currentLang}\n`;
1216
1223
  }
1217
1224
  if (tagValidationResponse?.missingTags?.length > 0) {
1218
1225
  errorString += `${this.props.intl.formatMessage(messages.missingTags)} ${tagValidationResponse.missingTags.toString()}\n`;
1219
1226
  }
1227
+ if (tagValidationResponse?.unsupportedTags?.length > 0) {
1228
+ errorString += `${this.props.intl.formatMessage(messages.unsupportedTags)} ${tagValidationResponse.unsupportedTags.toString()}\n`;
1229
+ }
1220
1230
  if (tagValidationResponse?.isBraceError){
1221
1231
  errorString += this.props.intl.formatMessage(globalMessages.unbalanacedCurlyBraces);
1222
1232
  }
1223
1233
  if (tagValidationResponse?.isContentEmpty) {
1224
1234
  errorString += this.props.intl.formatMessage(messages.emailBodyEmptyError);
1225
1235
  // Adds a bypass for cases where content is initially empty in the creation flow.
1226
- if(this.isLiquidFlowSupportedByChannel()){
1236
+ if(this.liquidFlow()){
1227
1237
  errorString = "";
1228
1238
  isLiquidValid = true;
1229
1239
  }
@@ -1235,7 +1245,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1235
1245
  }
1236
1246
  }
1237
1247
  if (errorString) {
1238
- if (this.isLiquidFlowSupportedByChannel()) {
1248
+ if (this.liquidFlow()) {
1239
1249
  this.setState(
1240
1250
  (prevState) => ({
1241
1251
  liquidErrorMessage: {
@@ -1262,7 +1272,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1262
1272
  });
1263
1273
  }
1264
1274
 
1265
- const isTemplateValid = this.isLiquidFlowSupportedByChannel() ? isLiquidValid : isValid;
1275
+ const isTemplateValid = this.liquidFlow() ? isLiquidValid : isValid;
1266
1276
  //Updating the state with the error data
1267
1277
  this.setState((prevState) => ({
1268
1278
  isFormValid: isTemplateValid,
@@ -1321,7 +1331,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1321
1331
  }
1322
1332
  onSubmitWrapper = (args) => {
1323
1333
  const {singleTab = null} = args || {};
1324
- if (this.isLiquidFlowSupportedByChannel()) {
1334
+ if (this.liquidFlow()) {
1325
1335
  // For MPUSH, we need to validate both Android and iOS content separately
1326
1336
  if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
1327
1337
  this.validateFormBuilderMPush(this.state.formData, singleTab);
@@ -1365,6 +1375,10 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1365
1375
  messages,
1366
1376
  onError,
1367
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 : {},
1368
1382
  skipTags: this.skipTags.bind(this)
1369
1383
  });
1370
1384
  } else {
@@ -1433,6 +1447,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1433
1447
  getLiquidTags: this.props.actions.getLiquidTags,
1434
1448
  formatMessage: this.props.intl.formatMessage,
1435
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,
1436
1457
  singleTab: singleTab?.toUpperCase(),
1437
1458
  });
1438
1459
  }
@@ -1487,41 +1508,116 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1487
1508
  });
1488
1509
  }
1489
1510
 
1490
- 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';
1491
1513
  let currentModule = this.props.location.query.module ? this.props.location.query.module : 'default';
1492
1514
  if (this.props.tagModule) {
1493
1515
  currentModule = this.props.tagModule;
1494
1516
  }
1495
1517
  const tags = tagsParam ? tagsParam : this.props.tags;
1496
- const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content;
1497
- const isOutboundModule = (currentModule || '').toUpperCase() === OUTBOUND;
1498
-
1499
- const initialMissingTags = (tags && tags.length && !isFullMode && isEmail && isOutboundModule && !isEmailUnsubscribeTagOptional() && !hasUnsubscribeTag(content))
1500
- ? ['unsubscribe']
1501
- : [];
1502
-
1503
- const response = validateTagsCore({
1504
- contentForBraceCheck: contentForValidation,
1505
- contentForUnsubscribeScan: content,
1506
- tags,
1507
- currentModule,
1508
- isFullMode,
1509
- initialMissingTags, // [] or ['unsubscribe']; core uses this instead of definition-based when provided
1510
- skipTagsFn: this.skipTags.bind(this),
1511
- includeIsContentEmpty: true,
1512
- });
1513
-
1514
- // 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.
1515
- const validString = /\S/.test(contentForValidation);
1516
- if (isEmailUnsubscribeTagOptional() && isEmail && isOutboundModule) {
1517
- const missingTagIndex = response.missingTags.indexOf('unsubscribe');
1518
- if (missingTagIndex !== -1) {
1519
- 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
+ }
1520
1572
  }
1521
- if (!validString) {
1522
- 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
+ }
1523
1615
  }
1524
1616
  }
1617
+ if(!validateIfTagClosed(contentForValidation)){
1618
+ response.isBraceError = true;
1619
+ response.valid = false;
1620
+ }
1525
1621
  return response;
1526
1622
  }
1527
1623
  /* eslint-enable */
@@ -2754,21 +2850,21 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2754
2850
 
2755
2851
 
2756
2852
  getMissingOrUnsupportedTagsName = (content = '', type) => {
2757
- const { MISSING_TAGS } = tagsTypes;
2853
+ const { MISSING_TAGS, UNSUPPORTED_TAGS} = tagsTypes;
2758
2854
  const tagValidationResponse = this.validateTags(content);
2759
- if (type && type === MISSING_TAGS) {
2760
- return (tagValidationResponse[type] || []).join(', ').toString();
2855
+ if (type && (type === MISSING_TAGS || type === UNSUPPORTED_TAGS)) {
2856
+ return tagValidationResponse[type].join(', ').toString();
2761
2857
  }
2762
2858
  return null;
2763
2859
  };
2764
2860
 
2765
2861
  renderTextAreaContent = (styling, columns, val, isVersionEnable, rows, cols, offset = 0) => {
2766
2862
  const { checkValidation, errorData, currentTab, formData } = this.state;
2767
- const { MISSING_TAGS } = tagsTypes;
2863
+ const { MISSING_TAGS, UNSUPPORTED_TAGS } = tagsTypes;
2768
2864
  const errorType = (isVersionEnable ? errorData[`${currentTab - 1}`][val.id] : errorData[val.id]);
2769
2865
  const ifError = checkValidation && errorType;
2770
2866
  const messageContent = isVersionEnable ? formData[`${currentTab - 1}`][val.id] : formData[val.id];
2771
- const { MISSING_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2867
+ const { MISSING_TAG_ERROR, UNSUPPORTED_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
2772
2868
  const { formatMessage } = this.props.intl;
2773
2869
 
2774
2870
  const { accessibleFeatures = [] } = this.props.currentOrgDetails || {};
@@ -2781,6 +2877,9 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2781
2877
  case MISSING_TAG_ERROR:
2782
2878
  errorMessageText = formatMessage(messages.missingTagsValidationError, {missingTags: this.getMissingOrUnsupportedTagsName(messageContent, MISSING_TAGS)});
2783
2879
  break;
2880
+ case UNSUPPORTED_TAG_ERROR:
2881
+ errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags: this.getMissingOrUnsupportedTagsName(messageContent, UNSUPPORTED_TAGS)});
2882
+ break;
2784
2883
  case TAG_BRACKET_COUNT_MISMATCH_ERROR:
2785
2884
  errorMessageText = formatMessage(globalMessages.unbalanacedCurlyBraces);
2786
2885
  break;
@@ -2790,8 +2889,12 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2790
2889
  default:
2791
2890
  break;
2792
2891
  }
2892
+
2893
+ if (this.props.restrictPersonalization && hasPersonalizationTags(messageContent)) {
2894
+ errorMessageText = formatMessage(messages.personalizationTagsErrorMessage);
2895
+ }
2793
2896
  const prevErrorMessage = this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.[0];
2794
- if (prevErrorMessage !== errorMessageText && errorMessageText && this.isLiquidFlowSupportedByChannel()) {
2897
+ if (prevErrorMessage !== errorMessageText && errorMessageText && this.liquidFlow()) {
2795
2898
  this.setState((prevState) => ({
2796
2899
  liquidErrorMessage: {
2797
2900
  ...prevState.liquidErrorMessage,
@@ -2802,10 +2905,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2802
2905
  this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
2803
2906
  });
2804
2907
  }
2805
- // Always render the textarea regardless of the liquid error state update above.
2806
- // Previously the textarea was inside the `else` branch, so it was not rendered during the
2807
- // one render cycle when the liquid error message was first set – causing the input to lose
2808
- // focus whenever a brace/tag error first appeared.
2908
+ else{
2809
2909
  if (styling === 'semantic') {
2810
2910
  columns.push(
2811
2911
  <CapColumn key="input" span={val.width} offset={offset}>
@@ -2813,7 +2913,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2813
2913
  id={val.id}
2814
2914
  placeholder={val.placeholder ? val.placeholder : ''}
2815
2915
  className={`${ifError ? 'error-form-builder' : ''}`}
2816
- errorMessage={errorMessageText && !this.isLiquidFlowSupportedByChannel() ? errorMessageText : ''}
2916
+ errorMessage={errorMessageText && !this.liquidFlow() ? errorMessageText : ''}
2817
2917
  label={val.label}
2818
2918
  autosize={val.autosize ? val.autosizeParams : false}
2819
2919
  onChange={(e) => this.updateFormData(e.target.value, val)}
@@ -2844,6 +2944,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2844
2944
  </CapColumn>
2845
2945
  );
2846
2946
  }
2947
+ }
2847
2948
  };
2848
2949
 
2849
2950
 
@@ -2957,6 +3058,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2957
3058
  userLocale={this.props.userLocale}
2958
3059
  selectedOfferDetails={this.props.selectedOfferDetails}
2959
3060
  eventContextTags={this.props?.eventContextTags}
3061
+ restrictPersonalization={this.props.restrictPersonalization}
2960
3062
  />
2961
3063
  </CapColumn>
2962
3064
  );
@@ -2991,6 +3093,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2991
3093
  inputProps={val.inputProps || {}}
2992
3094
  showInput={val.showInput !== false}
2993
3095
  showTagList={val.showTagList !== false}
3096
+ restrictPersonalization={this.props.restrictPersonalization}
2994
3097
  />
2995
3098
  </CapColumn>
2996
3099
  );
@@ -3367,8 +3470,15 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3367
3470
  ifError = this.state.checkValidation && (isVersionEnable ? this.state.errorData[`${this.state.currentTab - 1}`][val.id] : this.state.errorData[val.id]);
3368
3471
  const { TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
3369
3472
  const { formatMessage } = this.props.intl;
3370
- let errorMessageText = errorType === TAG_BRACKET_COUNT_MISMATCH_ERROR ? formatMessage(globalMessages.unbalanacedCurlyBraces) :(val.errorMessage && ifError ? val.errorMessage : '');
3371
3473
  const value = isVersionEnable ? this.state.formData[`${this.state.currentTab - 1}`][val.id] : this.state.formData[val.id];
3474
+ // Show personalization error for title field inline (same as message textarea) when restrictPersonalization is true
3475
+ const hasPersonalizationError = this.props.restrictPersonalization && hasPersonalizationTags(value);
3476
+ if (hasPersonalizationError) {
3477
+ ifError = true;
3478
+ }
3479
+ let errorMessageText = hasPersonalizationError
3480
+ ? formatMessage(messages.personalizationTagsErrorMessage)
3481
+ : (errorType === TAG_BRACKET_COUNT_MISMATCH_ERROR ? formatMessage(globalMessages.unbalanacedCurlyBraces) : (val.errorMessage && ifError ? val.errorMessage : ''));
3372
3482
  if (styling === 'semantic') {
3373
3483
  columns.push(
3374
3484
  <CapColumn key={val.id} span={val.width} offset={val.offset} style={val.style || {}}>
@@ -3619,6 +3729,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3619
3729
  selectedOfferDetails={this.props.selectedOfferDetails}
3620
3730
  channel={channel}
3621
3731
  eventContextTags={this.props?.eventContextTags}
3732
+ restrictPersonalization={this.props.restrictPersonalization}
3622
3733
  />
3623
3734
  </CapColumn>
3624
3735
  );
@@ -3670,6 +3781,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3670
3781
  inputProps={val.inputProps || {}}
3671
3782
  showInput={val.showInput !== false}
3672
3783
  showTagList={val.showTagList !== false}
3784
+ restrictPersonalization={this.props.restrictPersonalization}
3673
3785
  />
3674
3786
  </CapColumn>
3675
3787
  );
@@ -3895,7 +4007,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3895
4007
  <CapColumn
3896
4008
  style={val.colStyle ? val.colStyle : {border : ""}}
3897
4009
  span={val.width}
3898
- 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"} `}
3899
4011
  >
3900
4012
  <CKEditor
3901
4013
  id={val.id}
@@ -3939,7 +4051,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3939
4051
  isModuleFilterEnabled = this.props.isFullMode;
3940
4052
  }
3941
4053
  columns.push(
3942
- <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"}`}>
3943
4055
  <BeeEditor
3944
4056
  uid={uuid}
3945
4057
  tokenData={beeToken}
@@ -4225,7 +4337,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4225
4337
 
4226
4338
 
4227
4339
  return (
4228
- <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)} >
4229
4341
  <CapRow>
4230
4342
  {this.props.schema && this.renderForm()}
4231
4343
  <SlideBox
@@ -4313,6 +4425,7 @@ FormBuilder.propTypes = {
4313
4425
  forwardedTags: PropTypes.object.isRequired,
4314
4426
  isLoyaltyModule: PropTypes.bool.isRequired,
4315
4427
  isTestAndPreviewMode: PropTypes.bool, // Add new prop type
4428
+ restrictPersonalization: PropTypes.bool,
4316
4429
  };
4317
4430
 
4318
4431
  const mapStateToProps = createStructuredSelector({