@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
@@ -161,6 +161,15 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
161
161
  const newFormData = cloneDeep(formData);
162
162
  const {templateCta} = this.state;
163
163
  const { defaultData = {}, isFullMode, showTemplateName} = this.props;
164
+
165
+ // Check for personalization tokens if restriction is enabled and notify parent
166
+ if (this.props.restrictPersonalization) {
167
+ const hasTokens = this.checkForPersonalizationTokens(newFormData);
168
+ if (this.props.onPersonalizationTokensChange) {
169
+ this.props.onPersonalizationTokensChange(hasTokens);
170
+ }
171
+ }
172
+
164
173
  if (!isEmpty(templateCta)) {
165
174
  newFormData[this.state.currentTab - 1][`secondary-cta-${this.state.currentTab - 1}-label`] = get(templateCta, 'name');
166
175
  newFormData[this.state.currentTab - 1][`secondary-cta-${this.state.currentTab - 1}-action`] = get(templateCta, 'ctaTemplateDetails[0].buttonText');
@@ -1502,8 +1511,40 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
1502
1511
  this.injectEvents(schema);
1503
1512
  };
1504
1513
 
1514
+ checkForPersonalizationTokens = (formData) => {
1515
+ // Check for {{ }} (liquid tags) and [ ] (event context tags) in mobile push content
1516
+ const tokenRegex = /\{\{[\s\S]*?\}\}|\[[\s\S]*?\]/;
1517
+
1518
+ // Check all tabs/versions for personalization tokens
1519
+ if (formData && typeof formData === 'object') {
1520
+ for (const key in formData) {
1521
+ const tabData = formData[key];
1522
+ if (tabData && typeof tabData === 'object') {
1523
+ for (const fieldKey in tabData) {
1524
+ const fieldValue = tabData[fieldKey];
1525
+ if (typeof fieldValue === 'string' && tokenRegex.test(fieldValue)) {
1526
+ return true;
1527
+ }
1528
+ }
1529
+ }
1530
+ }
1531
+ }
1532
+ return false;
1533
+ };
1534
+
1505
1535
  saveFormData = (formData) => {
1506
1536
  //this function gets called from form bulder only when the form data is valid
1537
+
1538
+ // Check for personalization tokens if restriction is enabled
1539
+ if (this.props.restrictPersonalization) {
1540
+ const hasTokens = this.checkForPersonalizationTokens(formData);
1541
+ if (hasTokens) {
1542
+ const message = this.props.intl.formatMessage(messages.personalizationTokensErrorMessage);
1543
+ CapNotification.error({message, key: 'personalizationTokensError'});
1544
+ return;
1545
+ }
1546
+ }
1547
+
1507
1548
  const obj = this.getTransformedData(formData);
1508
1549
  const content = getContent(obj);
1509
1550
  const {
@@ -1907,6 +1948,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
1907
1948
  isFullMode={this.props.isFullMode}
1908
1949
  eventContextTags={this.props?.eventContextTags}
1909
1950
  messageDetails={this.props?.messageDetails}
1951
+ restrictPersonalization={this.props.restrictPersonalization}
1910
1952
  />
1911
1953
  </CapColumn>
1912
1954
  {this.props.iosCtasData && this.state.showIosCtaTable &&
@@ -2011,6 +2053,9 @@ Create.propTypes = {
2011
2053
  showTestAndPreviewSlidebox: PropTypes.bool,
2012
2054
  handleTestAndPreview: PropTypes.func,
2013
2055
  handleCloseTestAndPreview: PropTypes.func,
2056
+ restrictPersonalization: PropTypes.bool,
2057
+ isAnonymousType: PropTypes.bool,
2058
+ onPersonalizationTokensChange: PropTypes.func,
2014
2059
  };
2015
2060
 
2016
2061
  const mapStateToProps = createStructuredSelector({
@@ -342,4 +342,8 @@ export default defineMessages({
342
342
  id: 'creatives.containersV2.MobilePush.Create.thisSectionDisabledHoverText',
343
343
  defaultMessage: 'This section is being revamped. Till then it will remain disabled.',
344
344
  },
345
+ "personalizationTokensErrorMessage": {
346
+ id: 'creatives.containersV2.MobilePush.Create.personalizationTokensErrorMessage',
347
+ defaultMessage: 'Personalization tags are not supported for anonymous customers. Please remove the tags.',
348
+ },
345
349
  });
@@ -246,6 +246,15 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
246
246
  onFormDataChange = (formData, tabCount, currentTab, inputField) => {
247
247
  const newFormData = _.cloneDeep(formData);
248
248
  const {templateCta} = this.state;
249
+
250
+ // Check for personalization tokens if restriction is enabled and notify parent
251
+ if (this.props.restrictPersonalization) {
252
+ const hasTokens = this.checkForPersonalizationTokens(newFormData);
253
+ if (this.props.onPersonalizationTokensChange) {
254
+ this.props.onPersonalizationTokensChange(hasTokens);
255
+ }
256
+ }
257
+
249
258
  if (!_.isEmpty(templateCta) && this.state.currentTab === 2) {
250
259
  newFormData[this.state.currentTab - 1][`secondary-cta-${this.state.currentTab - 1}-label`] = get(templateCta, 'name');
251
260
  newFormData[this.state.currentTab - 1][`secondary-cta-${this.state.currentTab - 1}-action`] = get(templateCta, 'ctaTemplateDetails[0].buttonText');
@@ -1687,10 +1696,42 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
1687
1696
  };
1688
1697
 
1689
1698
  saveFormData = (formData) => {
1699
+ // Check for personalization tokens if restriction is enabled
1700
+ if (this.props.restrictPersonalization) {
1701
+ const hasTokens = this.checkForPersonalizationTokens(formData);
1702
+ if (hasTokens) {
1703
+ const message = this.props.intl.formatMessage(messages.personalizationTokensErrorMessage);
1704
+ CapNotification.error({message, key: 'personalizationTokensError'});
1705
+ return;
1706
+ }
1707
+ }
1708
+
1690
1709
  const obj = this.getTransformedData(formData);
1691
1710
 
1692
1711
  this.props.actions.editTemplate(obj, this.onUpdateTemplateComplete);
1693
1712
  };
1713
+
1714
+ checkForPersonalizationTokens = (formData) => {
1715
+ // Check for {{ }} (liquid tags) and [ ] (event context tags) in mobile push content
1716
+ const tokenRegex = /\{\{[\s\S]*?\}\}|\[[\s\S]*?\]/;
1717
+
1718
+ // Check all tabs/versions for personalization tokens
1719
+ if (formData && typeof formData === 'object') {
1720
+ for (const key in formData) {
1721
+ const tabData = formData[key];
1722
+ if (tabData && typeof tabData === 'object') {
1723
+ for (const fieldKey in tabData) {
1724
+ const fieldValue = tabData[fieldKey];
1725
+ if (typeof fieldValue === 'string' && tokenRegex.test(fieldValue)) {
1726
+ return true;
1727
+ }
1728
+ }
1729
+ }
1730
+ }
1731
+ }
1732
+ return false;
1733
+ };
1734
+
1694
1735
  handleFrameTasks = (e) => {
1695
1736
  //
1696
1737
  const type = e.data;
@@ -2186,6 +2227,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
2186
2227
  hideTestAndPreviewBtn={this.props.hideTestAndPreviewBtn}
2187
2228
  isFullMode={this.props.isFullMode}
2188
2229
  eventContextTags={this.props?.eventContextTags}
2230
+ restrictPersonalization={this.props.restrictPersonalization}
2189
2231
  />;
2190
2232
  })()}
2191
2233
  </CapColumn>
@@ -2295,6 +2337,9 @@ Edit.propTypes = {
2295
2337
  showTestAndPreviewSlidebox: PropTypes.bool,
2296
2338
  handleTestAndPreview: PropTypes.func,
2297
2339
  handleCloseTestAndPreview: PropTypes.func,
2340
+ restrictPersonalization: PropTypes.bool,
2341
+ isAnonymousType: PropTypes.bool,
2342
+ onPersonalizationTokensChange: PropTypes.func,
2298
2343
  };
2299
2344
 
2300
2345
  const mapStateToProps = createStructuredSelector({
@@ -334,4 +334,8 @@ export default defineMessages({
334
334
  id: 'creatives.containersV2.MobilePush.Edit.thisSectionDisabledHoverText',
335
335
  defaultMessage: 'This section is being revamped. Till then it will remain disabled.',
336
336
  },
337
+ "personalizationTokensErrorMessage": {
338
+ id: 'creatives.containersV2.MobilePush.Edit.personalizationTokensErrorMessage',
339
+ defaultMessage: 'Personalization tags are not supported for anonymous customers. Please remove the tags.',
340
+ },
337
341
  });
@@ -24,6 +24,7 @@ import {
24
24
  import messages from "../messages";
25
25
  import MediaUploaders from "./MediaUploaders";
26
26
  import CtaButtons from "./CtaButtons";
27
+ import { hasPersonalizationTags } from "../../../utils/commonUtils";
27
28
 
28
29
  const PlatformContentFields = ({
29
30
  deviceType,
@@ -40,8 +41,17 @@ const PlatformContentFields = ({
40
41
  tags,
41
42
  injectedTags,
42
43
  selectedOfferDetails,
44
+ // new prop to disable personalization features for anonymous users
45
+ restrictPersonalization = false,
43
46
  }) => {
44
47
  const { title: titleError, message: messageError } = errors;
48
+
49
+ const titleErrorToShow = titleError || (restrictPersonalization && hasPersonalizationTags(content.title)
50
+ ? formatMessage(messages.personalizationTagsErrorMessage)
51
+ : "");
52
+ const messageErrorToShow = messageError || (restrictPersonalization && hasPersonalizationTags(content.message)
53
+ ? formatMessage(messages.personalizationTagsErrorMessage)
54
+ : "");
45
55
  const {
46
56
  handleTitleChange,
47
57
  handleMessageChange,
@@ -139,14 +149,19 @@ const PlatformContentFields = ({
139
149
  );
140
150
 
141
151
  const getTagList = useCallback(
142
- (index) => (
143
- <TagList
144
- {...tagListProps}
145
- onTagSelect={(value) => onTagSelect(value, index)}
146
- onContextChange={handleOnTagsContextChange}
147
- />
148
- ),
149
- [tagListProps, onTagSelect, handleOnTagsContextChange]
152
+ (index) => {
153
+ const disableMsg = restrictPersonalization ? formatMessage(messages.personalizationNotSupportedAnonymous) : undefined;
154
+ return (
155
+ <TagList
156
+ {...tagListProps}
157
+ disabled={restrictPersonalization}
158
+ disableTooltipMsg={disableMsg}
159
+ onTagSelect={(value) => onTagSelect(value, index)}
160
+ onContextChange={handleOnTagsContextChange}
161
+ />
162
+ );
163
+ },
164
+ [tagListProps, onTagSelect, handleOnTagsContextChange, restrictPersonalization, formatMessage]
150
165
  );
151
166
 
152
167
  const onButtonTagSelect = useCallback(
@@ -178,9 +193,9 @@ const PlatformContentFields = ({
178
193
  size="default"
179
194
  isRequired
180
195
  errorMessage={
181
- titleError && (
196
+ titleErrorToShow && (
182
197
  <CapError className="mobile-push-template-title-error">
183
- {titleError}
198
+ {titleErrorToShow}
184
199
  </CapError>
185
200
  )
186
201
  }
@@ -202,9 +217,9 @@ const PlatformContentFields = ({
202
217
  size="default"
203
218
  isRequired
204
219
  errorMessage={
205
- messageError && (
220
+ messageErrorToShow && (
206
221
  <CapError className="mobile-push-template-message-error">
207
- {messageError}
222
+ {messageErrorToShow}
208
223
  </CapError>
209
224
  )
210
225
  }
@@ -314,6 +329,10 @@ const PlatformContentFields = ({
314
329
  tags={tags}
315
330
  injectedTags={injectedTags}
316
331
  selectedOfferDetails={selectedOfferDetails}
332
+ disabled={restrictPersonalization}
333
+ disableTooltipMsg={
334
+ restrictPersonalization ? formatMessage(messages.personalizationNotSupportedAnonymous) : undefined
335
+ }
317
336
  />
318
337
  </CapRow>
319
338
  <CapInput
@@ -389,6 +408,11 @@ PlatformContentFields.propTypes = {
389
408
  linkProps: PropTypes.object.isRequired,
390
409
  sameContent: PropTypes.bool.isRequired,
391
410
  formatMessage: PropTypes.func.isRequired,
411
+ restrictPersonalization: PropTypes.bool,
412
+ };
413
+
414
+ PlatformContentFields.defaultProps = {
415
+ restrictPersonalization: false,
392
416
  };
393
417
 
394
418
  export default PlatformContentFields;
@@ -11,25 +11,29 @@ import {
11
11
  jest.mock("@capillarytech/cap-ui-library/CapRow", () => ({ children }) => <div>{children}</div>);
12
12
  jest.mock("@capillarytech/cap-ui-library/CapColumn", () => ({ children }) => <div>{children}</div>);
13
13
  jest.mock("@capillarytech/cap-ui-library/CapInput", () => {
14
- const MockCapInput = ({ value, onChange, errorMessage, error, ...props }) => (
14
+ const MockCapInput = ({
15
+ value, onChange, errorMessage, error, ...props
16
+ }) => (
15
17
  <div>
16
18
  <input value={value || ""} onChange={onChange} error={error} {...props} />
17
19
  {(errorMessage || error) && <div data-testid="error-message">{errorMessage || error}</div>}
18
20
  </div>
19
21
  );
20
-
21
- MockCapInput.TextArea = ({ value, onChange, errorMessage, error, ...props }) => (
22
+
23
+ MockCapInput.TextArea = ({
24
+ value, onChange, errorMessage, error, ...props
25
+ }) => (
22
26
  <div>
23
- <textarea
24
- value={value || ""}
25
- onChange={onChange}
27
+ <textarea
28
+ value={value || ""}
29
+ onChange={onChange}
26
30
  data-testid="message-textarea"
27
31
  {...props}
28
32
  />
29
33
  {(errorMessage || error) && <div data-testid="error-message">{errorMessage || error}</div>}
30
34
  </div>
31
35
  );
32
-
36
+
33
37
  return MockCapInput;
34
38
  });
35
39
  jest.mock("@capillarytech/cap-ui-library/CapHeading", () => ({ children }) => <div>{children}</div>);
@@ -56,17 +60,19 @@ jest.mock("@capillarytech/cap-ui-library/CapLabel", () => ({ children }) => <lab
56
60
  jest.mock("@capillarytech/cap-ui-library/CapInfoNote", () => ({ message }) => <div data-testid="info-note">{message}</div>);
57
61
 
58
62
  // Mock child components
59
- jest.mock("../../../TagList", () => ({ onTagSelect, onContextChange, label, ...props }) => (
60
- <div data-testid="tag-list">
61
- <button
62
- type="button"
63
+ jest.mock("../../../TagList", () => ({
64
+ onTagSelect, onContextChange, label, disabled, disableTooltipMsg, ...props
65
+ }) => (
66
+ <div data-testid="tag-list" data-disabled={disabled || false} data-tooltip={disableTooltipMsg || ''}>
67
+ <button
68
+ type="button"
63
69
  onClick={() => onTagSelect && onTagSelect('test_tag')}
64
70
  data-testid="tag-select-button"
65
71
  >
66
72
  Select Tag
67
73
  </button>
68
- <button
69
- type="button"
74
+ <button
75
+ type="button"
70
76
  onClick={() => onContextChange && onContextChange('test_context')}
71
77
  data-testid="tag-context-button"
72
78
  >
@@ -99,6 +105,8 @@ jest.mock("../../messages", () => ({
99
105
  deepLinkKeysPlaceholder: { id: "deepLinkKeysPlaceholder", defaultMessage: "Enter {key}" },
100
106
  deepLinkKeysRequired: { id: "deepLinkKeysRequired", defaultMessage: "Deep link keys are required" },
101
107
  addLabels: { id: "addLabels", defaultMessage: "Add labels" },
108
+ personalizationTagsErrorMessage: { id: "personalizationTagsErrorMessage", defaultMessage: "Personalization tags are not supported for anonymous customers, please remove the tags." },
109
+ personalizationNotSupportedAnonymous: { id: "personalizationNotSupportedAnonymous", defaultMessage: "Personalization tags are not supported for anonymous customers" },
102
110
  }));
103
111
 
104
112
  // Mock constants
@@ -171,6 +179,7 @@ describe("PlatformContentFields", () => {
171
179
  tags: ["tag1", "tag2"],
172
180
  injectedTags: [],
173
181
  selectedOfferDetails: null,
182
+ restrictPersonalization: false,
174
183
  };
175
184
 
176
185
  const renderComponent = (props = {}) => render(
@@ -255,6 +264,38 @@ describe("PlatformContentFields", () => {
255
264
 
256
265
  expect(getByTestId("cap-error")).toHaveTextContent("Message is required");
257
266
  });
267
+
268
+ it("should show inline personalization error when restricted and tokens present", () => {
269
+ const personalizationErrorMsg = "Personalization tags are not supported for anonymous customers";
270
+ const formatMessageStub = jest.fn((msg) => msg?.defaultMessage ?? "");
271
+ const { container } = renderComponent({
272
+ restrictPersonalization: true,
273
+ content: { ...defaultProps.content, message: "Hello {{user}}" },
274
+ formatMessage: formatMessageStub,
275
+ });
276
+ expect(container.textContent).toContain(personalizationErrorMsg);
277
+ expect(formatMessageStub).toHaveBeenCalledWith(
278
+ expect.objectContaining({ defaultMessage: expect.stringContaining("Personalization tags") })
279
+ );
280
+ });
281
+ });
282
+
283
+ describe("Personalization restriction", () => {
284
+ it("disables tag list and shows tooltip when restrictPersonalization true", () => {
285
+ const formatMessageForTooltip = jest.fn((msg) => msg?.defaultMessage ?? "");
286
+ const { getAllByTestId } = renderComponent({
287
+ restrictPersonalization: true,
288
+ formatMessage: formatMessageForTooltip,
289
+ });
290
+ const tagLists = getAllByTestId("tag-list");
291
+ expect(tagLists.length).toBeGreaterThan(0);
292
+ tagLists.forEach((tag) => {
293
+ expect(tag).toHaveAttribute('data-disabled', 'true');
294
+ });
295
+ expect(formatMessageForTooltip).toHaveBeenCalledWith(
296
+ expect.objectContaining({ defaultMessage: "Personalization tags are not supported for anonymous customers" })
297
+ );
298
+ });
258
299
  });
259
300
 
260
301
  describe("Media Type Selection", () => {
@@ -390,7 +431,7 @@ describe("PlatformContentFields", () => {
390
431
  // Deep link keys handling - covering lines 109-120, 134, 297-303
391
432
  it('should handle deep link keys with array from selection', () => {
392
433
  const mockDeepLink = [
393
- { value: 'test-deep-link', keys: ['key1', 'key2'] }
434
+ { value: 'test-deep-link', keys: ['key1', 'key2'] },
394
435
  ];
395
436
  const mockLinkProps = {
396
437
  deepLink: mockDeepLink,
@@ -428,7 +469,7 @@ describe("PlatformContentFields", () => {
428
469
 
429
470
  it('should handle deep link keys with single key from selection', () => {
430
471
  const mockDeepLink = [
431
- { value: 'test-deep-link', keys: 'single-key' }
472
+ { value: 'test-deep-link', keys: 'single-key' },
432
473
  ];
433
474
  const mockLinkProps = {
434
475
  deepLink: mockDeepLink,
@@ -466,7 +507,7 @@ describe("PlatformContentFields", () => {
466
507
 
467
508
  it('should handle deep link keys with no keys from selection but existing keys', () => {
468
509
  const mockDeepLink = [
469
- { value: 'test-deep-link', keys: [] } // No keys from selection
510
+ { value: 'test-deep-link', keys: [] }, // No keys from selection
470
511
  ];
471
512
  const mockLinkProps = {
472
513
  deepLink: mockDeepLink,
@@ -503,7 +544,7 @@ describe("PlatformContentFields", () => {
503
544
 
504
545
  it('should handle deep link keys with no keys at all', () => {
505
546
  const mockDeepLink = [
506
- { value: 'test-deep-link', keys: [] } // No keys from selection
547
+ { value: 'test-deep-link', keys: [] }, // No keys from selection
507
548
  ];
508
549
  const mockLinkProps = {
509
550
  deepLink: mockDeepLink,
@@ -540,7 +581,7 @@ describe("PlatformContentFields", () => {
540
581
 
541
582
  it('should handle deep link keys with string value instead of array', () => {
542
583
  const mockDeepLink = [
543
- { value: 'test-deep-link', keys: 'single-key' }
584
+ { value: 'test-deep-link', keys: 'single-key' },
544
585
  ];
545
586
  const mockLinkProps = {
546
587
  deepLink: mockDeepLink,
@@ -577,7 +618,7 @@ describe("PlatformContentFields", () => {
577
618
 
578
619
  it('should handle deep link keys with undefined deepLinkKeysValue', () => {
579
620
  const mockDeepLink = [
580
- { value: 'test-deep-link', keys: ['key1', 'key2'] }
621
+ { value: 'test-deep-link', keys: ['key1', 'key2'] },
581
622
  ];
582
623
  const mockLinkProps = {
583
624
  deepLink: mockDeepLink,
@@ -615,7 +656,7 @@ describe("PlatformContentFields", () => {
615
656
 
616
657
  it('should handle deep link keys placeholder with fallback', () => {
617
658
  const mockDeepLink = [
618
- { value: 'test-deep-link', keys: ['key1', 'key2'] } // Need keys to trigger the section
659
+ { value: 'test-deep-link', keys: ['key1', 'key2'] }, // Need keys to trigger the section
619
660
  ];
620
661
  const mockLinkProps = {
621
662
  deepLink: mockDeepLink,
@@ -655,7 +696,7 @@ describe("PlatformContentFields", () => {
655
696
 
656
697
  it('should handle deep link keys error display', () => {
657
698
  const mockDeepLink = [
658
- { value: 'test-deep-link', keys: ['key1', 'key2'] }
699
+ { value: 'test-deep-link', keys: ['key1', 'key2'] },
659
700
  ];
660
701
  const mockLinkProps = {
661
702
  deepLink: mockDeepLink,
@@ -727,7 +768,7 @@ describe("PlatformContentFields", () => {
727
768
  describe('Deep link query parameter handling', () => {
728
769
  it('should match deep link with query parameters', () => {
729
770
  const mockDeepLink = [
730
- { value: 'myapp://profile', keys: ['userId'] }
771
+ { value: 'myapp://profile', keys: ['userId'] },
731
772
  ];
732
773
  const mockLinkProps = {
733
774
  deepLink: mockDeepLink,
@@ -766,7 +807,7 @@ describe("PlatformContentFields", () => {
766
807
 
767
808
  it('should not match deep link when no base match exists with query parameters', () => {
768
809
  const mockDeepLink = [
769
- { value: 'myapp://settings', keys: ['theme'] }
810
+ { value: 'myapp://settings', keys: ['theme'] },
770
811
  ];
771
812
  const mockLinkProps = {
772
813
  deepLink: mockDeepLink,
@@ -805,7 +846,7 @@ describe("PlatformContentFields", () => {
805
846
 
806
847
  it('should handle multiple query parameters in deep link value', () => {
807
848
  const mockDeepLink = [
808
- { value: 'testapp://dashboard', keys: ['category', 'filter'] }
849
+ { value: 'testapp://dashboard', keys: ['category', 'filter'] },
809
850
  ];
810
851
  const mockLinkProps = {
811
852
  deepLink: mockDeepLink,
@@ -844,7 +885,7 @@ describe("PlatformContentFields", () => {
844
885
 
845
886
  it('should handle empty query parameters in deep link value', () => {
846
887
  const mockDeepLink = [
847
- { value: 'myapp://search', keys: ['query'] }
888
+ { value: 'myapp://search', keys: ['query'] },
848
889
  ];
849
890
  const mockLinkProps = {
850
891
  deepLink: mockDeepLink,
@@ -860,7 +901,7 @@ describe("PlatformContentFields", () => {
860
901
  linkType: 'DEEP_LINK',
861
902
  };
862
903
 
863
- const { getByText } = render(
904
+ const { getByText } = render(
864
905
  <IntlProvider locale="en">
865
906
  <PlatformContentFields
866
907
  deviceType="ANDROID"
@@ -884,7 +925,7 @@ describe("PlatformContentFields", () => {
884
925
 
885
926
  it('should handle tag selection for external link in buttons', () => {
886
927
  const mockHandleExternalLinkChange = jest.fn();
887
-
928
+
888
929
  // Create a test that actually renders the component and triggers the useCallback
889
930
  const { container } = renderComponent({
890
931
  content: {
@@ -89,13 +89,14 @@ import { PlatformContentFields } from "./components";
89
89
  import { CREATE, EDIT, TRACK_CREATE_MPUSH } from "../App/constants";
90
90
  import { validateExternalLink, validateDeepLink } from "./utils";
91
91
  import messages from "./messages";
92
- import { EXTERNAL_URL } from "../CreativesContainer/constants";
92
+ import { EXTERNAL_URL, MOBILE_PUSH } from "../CreativesContainer/constants";
93
93
  import createMobilePushPayloadWithIntl from "../../utils/createMobilePushPayload";
94
94
  import { MOBILEPUSH } from "../../v2Components/CapVideoUpload/constants";
95
95
  import { StyledCapTab } from "./style";
96
96
  import TestAndPreviewSlidebox from "../../v2Components/TestAndPreviewSlidebox";
97
- import { MOBILE_PUSH } from "../CreativesContainer/constants";
97
+
98
98
  import creativesMessages from "../CreativesContainer/messages";
99
+ import { error } from "jquery";
99
100
 
100
101
  // Helper function to extract deep link keys from URL where value equals key
101
102
  const extractDeepLinkKeys = (deepLinkValue) => {
@@ -506,6 +507,8 @@ const MobilePushNew = ({
506
507
  isGetFormData,
507
508
  getTemplateDetailsInProgress,
508
509
  onCreateComplete,
510
+ // new flag from parent - when true personalization via tags should be disabled
511
+ restrictPersonalization = false,
509
512
  }) => {
510
513
  const { formatMessage } = intl;
511
514
 
@@ -836,6 +839,14 @@ const MobilePushNew = ({
836
839
  if (!value || value.trim() === "") {
837
840
  error = formatMessage(messages.emptyTemplateDescErrorMessage);
838
841
  }
842
+ // personalization restriction check
843
+ if (
844
+ restrictPersonalization
845
+ && value
846
+ && ((value.includes("{{") && value.includes("}}")) || (value.includes("[") && value.includes("]")))
847
+ ) {
848
+ error = formatMessage(creativesMessages.personalizationTokensErrorMessage);
849
+ }
839
850
  return error || "";
840
851
  },
841
852
  [templateDescErrorHandler, formatMessage]
@@ -847,9 +858,17 @@ const MobilePushNew = ({
847
858
  if (!value || value.trim() === "") {
848
859
  error = formatMessage(messages.emptyTemplateDescErrorMessage);
849
860
  }
861
+ // personalization restriction check
862
+ if (
863
+ restrictPersonalization
864
+ && value
865
+ && ((value.includes("{{") && value.includes("}}")) || (value.includes("[") && value.includes("]")))
866
+ ) {
867
+ error = formatMessage(creativesMessages.personalizationTokensErrorMessage);
868
+ }
850
869
  return error || "";
851
870
  },
852
- [templateDescErrorHandler, formatMessage]
871
+ [templateDescErrorHandler, formatMessage, restrictPersonalization]
853
872
  );
854
873
 
855
874
  const handleOnTagsContextChange = useCallback(
@@ -2001,6 +2020,11 @@ const MobilePushNew = ({
2001
2020
  tags: tags || [],
2002
2021
  injectedTags: injectedTags || {},
2003
2022
  selectedOfferDetails,
2023
+ // disable tag button when personalization is restricted
2024
+ disabled: restrictPersonalization,
2025
+ disableTooltipMsg: restrictPersonalization
2026
+ ? formatMessage(creativesMessages.personalizationNotSupportedAnonymous)
2027
+ : undefined,
2004
2028
  };
2005
2029
 
2006
2030
  // Fix nested ternary for videoAssetList and gifAssetList
@@ -2101,6 +2125,7 @@ const MobilePushNew = ({
2101
2125
  tags={tags}
2102
2126
  injectedTags={injectedTags}
2103
2127
  selectedOfferDetails={selectedOfferDetails}
2128
+ restrictPersonalization={restrictPersonalization}
2104
2129
  />
2105
2130
  );
2106
2131
  }, [androidContent, iosContent, androidTitleError, iosTitleError, androidMessageError, iosMessageError, androidExternalLinkError, iosExternalLinkError, androidDeepLinkError, iosDeepLinkError, androidDeepLinkKeysError, iosDeepLinkKeysError, formatMessage, activeTab, imageSrc, isFullMode, imageData, androidAssetList, iosAssetList, videoState, videoData, location, tags, injectedTags, selectedOfferDetails, primaryButtonAndroid, secondaryButtonAndroid, primaryButtonIos, secondaryButtonIos, ctaData, deepLink, mobilePushActions, carouselActiveTabIndex, carouselLinkErrors, handleTitleChange, handleMessageChange, handleMediaTypeChange, handleActionOnClickChange, handleLinkTypeChange, handleDeepLinkChange, handleDeepLinkKeysChange, handleExternalLinkChange, onTagSelect, handleOnTagsContextChange, setUpdateMpushImageSrc, updateOnMpushImageReUpload, setUpdateMpushVideoSrc, updateOnMpushVideoReUpload, clearImageDataByMediaType, handleCarouselDataChange, updateCarouselLinkError, sameContent, updateHandler, deleteHandler]
@@ -2136,6 +2161,9 @@ const MobilePushNew = ({
2136
2161
  return panes;
2137
2162
  }, [isAndroidSupported, isIosSupported, renderContentFields, formatMessage]);
2138
2163
 
2164
+ const errorInTitle = activeTab === ANDROID ? androidTitleError : iosTitleError;
2165
+ const errorInMessage = activeTab === ANDROID ? androidMessageError : iosMessageError;
2166
+
2139
2167
  // Save button disabled logic: only check enabled platforms
2140
2168
  const isSaveDisabled = (
2141
2169
  (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim()))
@@ -2143,6 +2171,7 @@ const MobilePushNew = ({
2143
2171
  || templateNameError
2144
2172
  || Object.values(carouselLinkErrors).some((error) => error !== null && error !== "")
2145
2173
  || !isCarouselDataValid()
2174
+ || errorInTitle || errorInMessage
2146
2175
  );
2147
2176
 
2148
2177
  // Validation in handleSave: only show errors for enabled platforms
@@ -269,4 +269,12 @@ export default defineMessages({
269
269
  id: `${scope}.gifFileTypeError`,
270
270
  defaultMessage: 'Only GIF files are allowed',
271
271
  },
272
+ personalizationTagsErrorMessage: {
273
+ id: `${scope}.personalizationTagsErrorMessage`,
274
+ defaultMessage: 'Personalization tags are not supported for anonymous customers, please remove the tags.',
275
+ },
276
+ personalizationNotSupportedAnonymous: {
277
+ id: `${scope}.personalizationNotSupportedAnonymous`,
278
+ defaultMessage: `Personalization tags are not supported for anonymous customers`,
279
+ },
272
280
  });
@@ -72,7 +72,7 @@ export class MobilepushWrapper extends React.Component { // eslint-disable-line
72
72
  }
73
73
 
74
74
  render() {
75
- const {mobilePushCreateMode, step, getFormData, setIsLoadingContent, isGetFormData, query, isFullMode, showTemplateName, type, onValidationFail, onPreviewContentClicked, onTestContentClicked, templateData, eventContextTags = [], showTestAndPreviewSlidebox, handleTestAndPreview, handleCloseTestAndPreview} = this.props;
75
+ const {mobilePushCreateMode, step, getFormData, setIsLoadingContent, isGetFormData, query, isFullMode, showTemplateName, type, onValidationFail, onPreviewContentClicked, onTestContentClicked, templateData, eventContextTags = [], showTestAndPreviewSlidebox, handleTestAndPreview, handleCloseTestAndPreview, restrictPersonalization, isAnonymousType, onPersonalizationTokensChange} = this.props;
76
76
  const {templateName} = this.state;
77
77
  const isShowMobilepushCreate = !isEmpty(mobilePushCreateMode);
78
78
  return (
@@ -124,6 +124,9 @@ export class MobilepushWrapper extends React.Component { // eslint-disable-line
124
124
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
125
125
  handleTestAndPreview={handleTestAndPreview}
126
126
  handleCloseTestAndPreview={handleCloseTestAndPreview}
127
+ restrictPersonalization={restrictPersonalization}
128
+ isAnonymousType={isAnonymousType}
129
+ onPersonalizationTokensChange={onPersonalizationTokensChange}
127
130
  />
128
131
 
129
132
 
@@ -154,6 +157,9 @@ MobilepushWrapper.propTypes = {
154
157
  showTestAndPreviewSlidebox: PropTypes.bool,
155
158
  handleTestAndPreview: PropTypes.func,
156
159
  handleCloseTestAndPreview: PropTypes.func,
160
+ restrictPersonalization: PropTypes.bool,
161
+ isAnonymousType: PropTypes.bool,
162
+ onPersonalizationTokensChange: PropTypes.func,
157
163
  };
158
164
 
159
165
 
@@ -98,5 +98,5 @@
98
98
  }
99
99
 
100
100
  .create-dlt-msg {
101
- margin-left: 100px;
101
+ margin-left: 6.25rem;
102
102
  }