@capillarytech/creatives-library 8.0.286 → 8.0.287

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 (32) hide show
  1. package/package.json +1 -1
  2. package/utils/commonUtils.js +3 -0
  3. package/v2Components/CapTagList/index.js +6 -2
  4. package/v2Components/CapTagListWithInput/index.js +4 -0
  5. package/v2Components/FormBuilder/index.js +26 -3
  6. package/v2Components/FormBuilder/messages.js +4 -0
  7. package/v2Containers/CreativesContainer/SlideBoxContent.js +20 -0
  8. package/v2Containers/CreativesContainer/SlideBoxFooter.js +39 -3
  9. package/v2Containers/CreativesContainer/constants.js +6 -0
  10. package/v2Containers/CreativesContainer/index.js +32 -1
  11. package/v2Containers/CreativesContainer/messages.js +12 -0
  12. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +339 -0
  13. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +18 -0
  14. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +37 -0
  15. package/v2Containers/MobilePush/Create/index.js +45 -0
  16. package/v2Containers/MobilePush/Create/messages.js +4 -0
  17. package/v2Containers/MobilePush/Edit/index.js +45 -0
  18. package/v2Containers/MobilePush/Edit/messages.js +4 -0
  19. package/v2Containers/MobilePushNew/components/PlatformContentFields.js +36 -12
  20. package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +68 -27
  21. package/v2Containers/MobilePushNew/index.js +32 -3
  22. package/v2Containers/MobilePushNew/messages.js +8 -0
  23. package/v2Containers/MobilepushWrapper/index.js +7 -1
  24. package/v2Containers/SmsTrai/Create/index.scss +1 -1
  25. package/v2Containers/TagList/index.js +17 -1
  26. package/v2Containers/TagList/messages.js +4 -0
  27. package/v2Containers/TemplatesV2/index.js +43 -23
  28. package/v2Containers/Viber/index.scss +1 -1
  29. package/v2Containers/WebPush/Create/index.js +25 -6
  30. package/v2Containers/WebPush/Create/messages.js +8 -1
  31. package/v2Containers/WebPush/Create/utils/validation.js +16 -9
  32. package/v2Containers/WebPush/Create/utils/validation.test.js +28 -0
@@ -396,6 +396,17 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
396
396
  }
397
397
 
398
398
  render() {
399
+ const { restrictPersonalization, disabled, disableTooltipMsg, intl } = this.props;
400
+
401
+ // Compute disabled state and tooltip message
402
+ let isDisabled = disabled || false;
403
+ let tooltipMsg = disableTooltipMsg;
404
+
405
+ if (restrictPersonalization && !disabled) {
406
+ isDisabled = true;
407
+ tooltipMsg = intl.formatMessage(messages.personalizationNotSupportedAnonymous);
408
+ }
409
+
399
410
  return (
400
411
  <div className={this.props.className ? this.props.className : ''}>
401
412
  <CapTagList
@@ -411,7 +422,9 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
411
422
  modalProps={this.props.modalProps}
412
423
  currentOrgDetails={this.props.currentOrgDetails}
413
424
  channel={this.props.channel}
414
- disabled={this.props.disabled}
425
+ disabled={isDisabled}
426
+ // custom tooltip message to show when disabled
427
+ disableTooltipMsg={tooltipMsg}
415
428
  fetchingSchemaError={this?.state?.tagsError}
416
429
  popoverPlacement={this.props.popoverPlacement}
417
430
  />
@@ -445,6 +458,9 @@ TagList.propTypes = {
445
458
  fetchingSchemaError: PropTypes.bool,
446
459
  eventContextTags: PropTypes.array,
447
460
  popoverPlacement: PropTypes.string,
461
+ // message to show when Add Label button is disabled (e.g. personalization restriction)
462
+ disableTooltipMsg: PropTypes.string,
463
+ restrictPersonalization: PropTypes.bool,
448
464
  intl: PropTypes.shape({
449
465
  formatMessage: PropTypes.func.isRequired,
450
466
  locale: PropTypes.string,
@@ -15,4 +15,8 @@ export default defineMessages({
15
15
  id: `${scope}.entryEvent`,
16
16
  defaultMessage: 'Entry event',
17
17
  },
18
+ personalizationNotSupportedAnonymous: {
19
+ id: `${scope}.personalizationNotSupportedAnonymous`,
20
+ defaultMessage: 'Personalization tags are not supported for anonymous customers',
21
+ },
18
22
  });
@@ -12,7 +12,6 @@ import { createStructuredSelector } from 'reselect';
12
12
  import { bindActionCreators, compose } from 'redux';
13
13
  import { CapTab, CapCustomCard, CapButton, CapHeader, CapSpin, CapIcon, CapTooltip } from '@capillarytech/cap-ui-library';
14
14
  import { find, get } from 'lodash';
15
- import isEmpty from 'lodash/isEmpty';
16
15
  import Helmet from 'react-helmet';
17
16
 
18
17
  import { UserIsAuthenticated } from '../../utils/authWrapper';
@@ -58,6 +57,7 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
58
57
  cap = {},
59
58
  loyaltyMetaData = {},
60
59
  isLoyaltyModule = false,
60
+ isAnonymousType = false,
61
61
  } = props;
62
62
 
63
63
  const defaultPanes = {
@@ -89,44 +89,54 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
89
89
  key: 'wechat',
90
90
  };
91
91
  }
92
- let filteredPanes = Object.keys(defaultPanes)
93
- .filter((key) => !channelsToHide.includes(key)).reduce((obj = [], key) => {
94
- obj.push(defaultPanes[key]);
95
- return obj;
96
- }, []);
97
-
98
- if (isFullMode ) {
99
- filteredPanes.push({content: <div></div>, tab: intl.formatMessage(messages.gallery), key: 'assets'});
92
+ // Robust normalization function: converts camelCase, hyphens, spaces and mixed-case to snake_case lowercase
93
+ const normalizeChannel = (raw = '') => {
94
+ const str = (raw || '').toString();
95
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[^a-zA-Z0-9]+/g, '_').toLowerCase();
96
+ };
97
+
98
+ const normalizedChannelsToHideSet = new Set((channelsToHide || []).map((c) => normalizeChannel(c)));
99
+ const normalizedChannelsToDisableSet = new Set((channelsToDisable || []).map((c) => normalizeChannel(c)));
100
+
101
+ // Build filtered panes by examining each pane's `key` and checking against normalized hide set
102
+ let filteredPanes = Object.keys(defaultPanes).map((k) => defaultPanes[k]).filter((pane) => {
103
+ const paneKey = normalizeChannel(pane.key);
104
+ return !normalizedChannelsToHideSet.has(paneKey);
105
+ });
106
+
107
+ if (isFullMode) {
108
+ filteredPanes.push({ content: <div></div>, tab: intl.formatMessage(messages.gallery), key: 'assets' });
100
109
  } else {
101
- if (!channelsToHide.includes('callTask')) {
102
- filteredPanes.push({content: <div></div>, tab: intl.formatMessage(messages.callTask), key: 'call_task'});
110
+ // Add special-mode panes only when not hidden (use normalized checks)
111
+ if (!normalizedChannelsToHideSet.has('call_task')) {
112
+ filteredPanes.push({ content: <div></div>, tab: intl.formatMessage(messages.callTask), key: 'call_task' });
103
113
  }
104
- if (!channelsToHide.includes('FTP')) {
105
- filteredPanes.push({content: <></>, tab: intl.formatMessage(messages.FTP), key: 'ftp'});
114
+ if (!normalizedChannelsToHideSet.has('ftp')) {
115
+ filteredPanes.push({ content: <></>, tab: intl.formatMessage(messages.FTP), key: 'ftp' });
106
116
  defaultChannel = 'FTP';
107
117
  }
108
118
 
109
119
  // Create a local copy of COMMON_CHANNELS to avoid mutating the imported array
110
120
  const channels = [...COMMON_CHANNELS];
111
- const { actionName = ''} = loyaltyMetaData;
121
+ const { actionName = '' } = loyaltyMetaData;
112
122
  if (isLoyaltyModule && actionName === LOYALTY_SUPPORTED_ACTION) {
113
123
  channels.push(WHATSAPP, ZALO);
114
124
  }
115
125
 
116
- // we only show channels which other than COMMON_CHANNELS
117
- // if it is coming in enableNewChannels array
126
+ // we only show channels other than COMMON_CHANNELS if they are present in enableNewChannels
118
127
  filteredPanes = filteredPanes.filter((item) => {
119
- const channel = item.key;
120
- if (!channels.includes(channel)) {
121
- return enableNewChannels.includes(channel.toUpperCase());
128
+ const channelKey = normalizeChannel(item.key);
129
+ if (!channels.includes(channelKey)) {
130
+ return enableNewChannels.includes(channelKey.toUpperCase());
122
131
  }
123
132
  return true;
124
133
  });
125
134
  }
126
135
 
127
136
 
128
- filteredPanes = filteredPanes.map( (pane) => {
129
- if (channelsToDisable.includes(pane.key)) {
137
+ filteredPanes = filteredPanes.map((pane) => {
138
+ const paneKeyNorm = normalizeChannel(pane.key);
139
+ if (normalizedChannelsToDisableSet.has(paneKeyNorm)) {
130
140
  // eslint-disable-next-line no-param-reassign
131
141
  pane.disabled = true;
132
142
  if (pane.key === 'facebook' && showDisabledFBInfo) {
@@ -151,10 +161,18 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
151
161
  filteredPanes = hideEngagementChannel ? filteredPanes?.filter((pane) => [EMAIL, LINE, ASSETS].includes(pane?.key) && pane) : filteredPanes;
152
162
  defaultChannel = hideEngagementChannel ? EMAIL : defaultChannel;
153
163
 
164
+ // If audience is anonymous, prefer mobilepush as default (if not hidden)
165
+ if (isAnonymousType) {
166
+ const mobilePushNorm = normalizeChannel('mobilepush');
167
+ if (!normalizedChannelsToHideSet.has(mobilePushNorm)) {
168
+ defaultChannel = 'mobilepush';
169
+ }
170
+ }
171
+
154
172
  const channel = ['sms', 'email', 'mobilepush', 'line', 'call_task'];
155
- if (!isEmpty(channelsToDisable)) {
173
+ if (normalizedChannelsToDisableSet.size > 0) {
156
174
  channel.some((ch) => {
157
- if (!channelsToDisable.includes(ch)) {
175
+ if (!normalizedChannelsToDisableSet.has(ch)) {
158
176
  defaultChannel = ch;
159
177
  return true;
160
178
  }
@@ -380,6 +398,8 @@ TemplatesV2.propTypes = {
380
398
  FTPMode: PropTypes.string,
381
399
  messageStrategy: PropTypes.string,
382
400
  currentOrgDetails: PropTypes.object,
401
+ restrictPersonalization: PropTypes.bool,
402
+ isAnonymousType: PropTypes.bool,
383
403
  };
384
404
 
385
405
  TemplatesV2.defaultProps = {
@@ -121,5 +121,5 @@
121
121
  margin-top: $CAP_SPACE_08;
122
122
  }
123
123
  .test-and-preview-button {
124
- margin-left: 100px;
124
+ margin-left: 6.25rem;
125
125
  }
@@ -85,6 +85,9 @@ const MemoizedTagList = memo(({
85
85
  eventContextTags,
86
86
  forwardedTags,
87
87
  onTagSelect,
88
+ restrictPersonalization = false,
89
+ disabled = false,
90
+ disableTooltipMsg,
88
91
  }) => (
89
92
  <TagList
90
93
  moduleFilterEnabled={moduleFilterEnabled}
@@ -97,6 +100,9 @@ const MemoizedTagList = memo(({
97
100
  eventContextTags={eventContextTags}
98
101
  forwardedTags={forwardedTags}
99
102
  onTagSelect={onTagSelect}
103
+ restrictPersonalization={restrictPersonalization}
104
+ disabled={disabled || restrictPersonalization}
105
+ disableTooltipMsg={disableTooltipMsg}
100
106
  />
101
107
  ), (prevProps, nextProps) => {
102
108
  // Custom comparison function for better memoization
@@ -111,6 +117,9 @@ const MemoizedTagList = memo(({
111
117
  && prevProps.eventContextTags === nextProps.eventContextTags
112
118
  && prevProps.forwardedTags === nextProps.forwardedTags
113
119
  && prevProps.onTagSelect === nextProps.onTagSelect
120
+ && prevProps.restrictPersonalization === nextProps.restrictPersonalization
121
+ && prevProps.disabled === nextProps.disabled
122
+ && prevProps.disableTooltipMsg === nextProps.disableTooltipMsg
114
123
  );
115
124
  });
116
125
 
@@ -144,6 +153,7 @@ const WebPushCreate = ({
144
153
  eventContextTags = [],
145
154
  templateActions: templateActionsProps,
146
155
  Templates,
156
+ restrictPersonalization = false,
147
157
  }) => {
148
158
  const { formatMessage } = intl;
149
159
 
@@ -285,8 +295,8 @@ const WebPushCreate = ({
285
295
  const validateTemplateName = useCallback((value) => validateTemplateNameUtil(value), []);
286
296
 
287
297
  const validateTitle = useCallback(
288
- (value) => validateTitleUtil(value, formatMessage, messages),
289
- [formatMessage],
298
+ (value) => validateTitleUtil(value, formatMessage, messages, restrictPersonalization),
299
+ [formatMessage, restrictPersonalization],
290
300
  );
291
301
 
292
302
  const validateUrl = useCallback(
@@ -309,8 +319,8 @@ const WebPushCreate = ({
309
319
 
310
320
 
311
321
  const validateMessageContent = useCallback(
312
- (value) => validateMessageContentUtil(value, formatMessage, messages, validationConfig, isFullMode),
313
- [formatMessage, validationConfig, isFullMode],
322
+ (value) => validateMessageContentUtil(value, formatMessage, messages, validationConfig, isFullMode, restrictPersonalization),
323
+ [formatMessage, validationConfig, isFullMode, restrictPersonalization],
314
324
  );
315
325
 
316
326
  useEffect(() => {
@@ -546,7 +556,7 @@ const WebPushCreate = ({
546
556
  // Pure validator that returns boolean without setting error state
547
557
  const validateFormSilent = () => {
548
558
  const templateNameInvalid = isFullMode && validateTemplateName(templateName);
549
- const titleValidation = validateTitle(notificationTitle);
559
+ const titleValidation = validateTitle(notificationTitle, restrictPersonalization);
550
560
  const messageValidation = validateMessageContent(message);
551
561
 
552
562
  return !(templateNameInvalid || titleValidation || messageValidation);
@@ -569,6 +579,7 @@ const WebPushCreate = ({
569
579
  return;
570
580
  }
571
581
 
582
+
572
583
  // Set flag to indicate save/edit operation has been initiated
573
584
  saveInitiatedRef.current = true;
574
585
 
@@ -825,8 +836,11 @@ const WebPushCreate = ({
825
836
  selectedOfferDetails,
826
837
  eventContextTags,
827
838
  forwardedTags,
839
+ restrictPersonalization,
840
+ disabled: restrictPersonalization,
841
+ disableTooltipMsg: restrictPersonalization ? formatMessage(messages.personalizationNotSupportedAnonymous) : undefined,
828
842
  }),
829
- [tags, injectedTags, selectedOfferDetails, eventContextTags, forwardedTags],
843
+ [tags, injectedTags, selectedOfferDetails, eventContextTags, forwardedTags, restrictPersonalization, formatMessage],
830
844
  );
831
845
 
832
846
  // Memoized TagList components with optimized props
@@ -881,6 +895,7 @@ const WebPushCreate = ({
881
895
  )
882
896
  )
883
897
  || (onClickBehaviour === ON_CLICK_BEHAVIOUR_OPTIONS.REDIRECT_TO_URL && (!redirectUrl.trim() || redirectUrlError))
898
+ || !!titleError || !!messageError
884
899
  ),
885
900
  [
886
901
  createTemplateInProgress,
@@ -906,6 +921,8 @@ const WebPushCreate = ({
906
921
  onClickBehaviour,
907
922
  redirectUrl,
908
923
  redirectUrlError,
924
+ titleError,
925
+ messageError,
909
926
  ],
910
927
  );
911
928
 
@@ -1064,6 +1081,7 @@ WebPushCreate.propTypes = {
1064
1081
  selectedOfferDetails: PropTypes.array,
1065
1082
  eventContextTags: PropTypes.array,
1066
1083
  templateActions: PropTypes.object,
1084
+ restrictPersonalization: PropTypes.bool,
1067
1085
  };
1068
1086
 
1069
1087
  WebPushCreate.defaultProps = {
@@ -1092,6 +1110,7 @@ WebPushCreate.defaultProps = {
1092
1110
  eventContextTags: [],
1093
1111
  templateActions: {},
1094
1112
  Templates: {},
1113
+ restrictPersonalization: false,
1095
1114
  };
1096
1115
 
1097
1116
  const mapStateToProps = createStructuredSelector({
@@ -207,5 +207,12 @@ export default defineMessages({
207
207
  id: `${scope}.templateIdMissingError`,
208
208
  defaultMessage: 'Unable to save template: Template ID is missing. Please refresh the page and try again.',
209
209
  },
210
+ personalizationTokensErrorMessage: {
211
+ id: `${scope}.personalizationTokensErrorMessage`,
212
+ defaultMessage: 'Personalization tags are not supported for anonymous customers. Please remove the tags.',
213
+ },
214
+ personalizationNotSupportedAnonymous: {
215
+ id: `${scope}.personalizationNotSupportedAnonymous`,
216
+ defaultMessage: 'Personalization tags are not supported for anonymous customers',
217
+ },
210
218
  });
211
-
@@ -1,6 +1,7 @@
1
1
  import { isValidHttpUrl } from './urlValidation';
2
2
  import { validateTags } from '../../../../utils/tagValidations';
3
3
  import globalMessages from '../../../Cap/messages';
4
+ import { hasPersonalizationTags } from '../../../../utils/commonUtils';
4
5
 
5
6
  /**
6
7
  * Validates template name (checks if empty)
@@ -16,10 +17,13 @@ export const validateTemplateName = (value) => !value || value.trim() === '';
16
17
  * @param {Object} messages - Message definitions
17
18
  * @returns {string} Error message if invalid, empty string if valid
18
19
  */
19
- export const validateTitle = (value, formatMessage, messages) => {
20
+ export const validateTitle = (value, formatMessage, messages, restrictPersonalization) => {
20
21
  if (!value || value.trim() === '') {
21
22
  return formatMessage(messages.titleRequired);
22
23
  }
24
+ if (restrictPersonalization && hasPersonalizationTags(value)) {
25
+ return formatMessage(messages.personalizationTokensErrorMessage);
26
+ }
23
27
  return '';
24
28
  };
25
29
 
@@ -34,11 +38,11 @@ export const validateUrl = (value, formatMessage, messages) => {
34
38
  if (!value || value.trim() === '') {
35
39
  return formatMessage(messages.urlRequired);
36
40
  }
37
-
41
+
38
42
  if (!isValidHttpUrl(value)) {
39
43
  return formatMessage(messages.urlInvalid);
40
44
  }
41
-
45
+
42
46
  return '';
43
47
  };
44
48
 
@@ -50,27 +54,30 @@ export const validateUrl = (value, formatMessage, messages) => {
50
54
  * @param {Object} validationConfig - Configuration for tag validation
51
55
  * @returns {string} Error message if invalid, empty string if valid
52
56
  */
53
- export const validateMessageContent = (value, formatMessage, messages, validationConfig, isFullMode) => {
57
+ export const validateMessageContent = (value, formatMessage, messages, validationConfig, isFullMode, restrictPersonalization) => {
54
58
  if (!value || value.trim() === '') {
55
59
  return formatMessage(messages.messageRequired);
56
60
  }
57
-
61
+
58
62
  const validationResponse = validateTags({
59
63
  content: value,
60
64
  ...validationConfig,
61
65
  isFullMode,
62
66
  }) || {};
63
-
67
+
64
68
  if (validationResponse?.unsupportedTags?.length) {
65
69
  return formatMessage(globalMessages.unsupportedTagsValidationError, {
66
70
  unsupportedTags: validationResponse.unsupportedTags.join(', '),
67
71
  });
68
72
  }
69
-
73
+
70
74
  if (validationResponse?.isBraceError) {
71
75
  return formatMessage(globalMessages.unbalanacedCurlyBraces);
72
76
  }
73
-
77
+
78
+ if (restrictPersonalization && hasPersonalizationTags(value)) {
79
+ return formatMessage(messages.personalizationTokensErrorMessage);
80
+ }
81
+
74
82
  return '';
75
83
  };
76
-
@@ -44,6 +44,9 @@ describe('validation', () => {
44
44
  messageRequired: {
45
45
  defaultMessage: 'Message is required',
46
46
  },
47
+ personalizationTokensErrorMessage: {
48
+ defaultMessage: 'Personalization tags are not supported for anonymous customers',
49
+ },
47
50
  };
48
51
 
49
52
  beforeEach(() => {
@@ -113,6 +116,17 @@ describe('validation', () => {
113
116
  const result = validateTitle(' Valid Title ', mockFormatMessage, mockMessages);
114
117
  expect(result).toBe('');
115
118
  });
119
+
120
+ it('should return personalization error when restrictPersonalization is true and title has personalization tags', () => {
121
+ const result = validateTitle(
122
+ 'Hello {{name}}',
123
+ mockFormatMessage,
124
+ mockMessages,
125
+ true
126
+ );
127
+ expect(result).toBe('Personalization tags are not supported for anonymous customers');
128
+ expect(mockFormatMessage).toHaveBeenCalledWith(mockMessages.personalizationTokensErrorMessage);
129
+ });
116
130
  });
117
131
 
118
132
  describe('validateUrl', () => {
@@ -278,6 +292,20 @@ describe('validation', () => {
278
292
  ...mockValidationConfig,
279
293
  });
280
294
  });
295
+
296
+ it('should return personalization error when restrictPersonalization is true and message has personalization tags', () => {
297
+ validateTags.mockReturnValue({});
298
+ const result = validateMessageContent(
299
+ 'Hello {{name}}',
300
+ mockFormatMessage,
301
+ mockMessages,
302
+ mockValidationConfig,
303
+ true,
304
+ true
305
+ );
306
+ expect(result).toBe('Personalization tags are not supported for anonymous customers');
307
+ expect(mockFormatMessage).toHaveBeenCalledWith(mockMessages.personalizationTokensErrorMessage);
308
+ });
281
309
  });
282
310
  });
283
311