@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
@@ -26,6 +26,10 @@ export default defineMessages({
26
26
  id: 'creatives.components.FormBuilder.missingTagsValidationError',
27
27
  defaultMessage: 'Missing tags: {missingTags}. Please add them to this message.',
28
28
  },
29
+ unsupportedTagsValidationError: {
30
+ id: 'creatives.components.FormBuilder.unsupportedTagsValidationError',
31
+ defaultMessage: 'Unsupported tags: {unsupportedTags}. Please remove them from this message.',
32
+ },
29
33
  genericTagsValidationError: {
30
34
  id: 'creatives.components.FormBuilder.genericTagsValidationError',
31
35
  defaultMessage: 'Please check the message content for unsupported/missing tags',
@@ -38,6 +42,10 @@ export default defineMessages({
38
42
  id: 'creatives.componentsV2.FormBuilder.missingTags',
39
43
  defaultMessage: 'Missing tags are:',
40
44
  },
45
+ unsupportedTags: {
46
+ id: 'creatives.componentsV2.FormBuilder.unsupportedTags',
47
+ defaultMessage: 'Unsupported tags are:',
48
+ },
41
49
  upload: {
42
50
  id: 'creatives.componentsV2.FormBuilder.upload',
43
51
  defaultMessage: 'Upload',
@@ -94,4 +102,8 @@ export default defineMessages({
94
102
  id: 'creatives.componentsV2.FormBuilder.base64ImageError',
95
103
  defaultMessage: 'Base64 images are not allowed. Please upload your image to a gallery and use it, or add the image URL instead.',
96
104
  },
105
+ personalizationTagsErrorMessage: {
106
+ id: `creatives.componentsV2.FormBuilder.personalizationTagsErrorMessage`,
107
+ defaultMessage: 'Personalization tags are not supported for anonymous customers, please remove the tags.',
108
+ },
97
109
  });
@@ -102,6 +102,7 @@ const HTMLEditor = forwardRef(({
102
102
  onTagSelect = null,
103
103
  onContextChange = null,
104
104
  globalActions = null,
105
+ isLiquidEnabled = false, // Controls Liquid tab visibility in ValidationTabs
105
106
  isFullMode = true, // Full mode vs library mode - controls layout and visibility
106
107
  onErrorAcknowledged = null, // Callback when user clicks redirection icon to acknowledge errors
107
108
  onValidationChange = null, // Callback when validation state changes (for parent to track errors)
@@ -572,6 +573,7 @@ const HTMLEditor = forwardRef(({
572
573
  content,
573
574
  layout,
574
575
  validation,
576
+ isLiquidEnabled,
575
577
  editorRef: getActiveEditorRef(),
576
578
  handleLabelInsert,
577
579
  handleSave,
@@ -591,6 +593,7 @@ const HTMLEditor = forwardRef(({
591
593
  content,
592
594
  layout,
593
595
  validation,
596
+ isLiquidEnabled,
594
597
  getActiveEditorRef,
595
598
  handleLabelInsert,
596
599
  handleSave,
@@ -779,6 +782,7 @@ HTMLEditor.propTypes = {
779
782
  onTagSelect: PropTypes.func,
780
783
  onContextChange: PropTypes.func, // Deprecated: use globalActions instead
781
784
  globalActions: PropTypes.object,
785
+ isLiquidEnabled: PropTypes.bool, // Controls Liquid tab visibility in validation
782
786
  isFullMode: PropTypes.bool, // Full mode vs library mode
783
787
  onErrorAcknowledged: PropTypes.func, // Callback when user clicks redirection icon to acknowledge errors
784
788
  onValidationChange: PropTypes.func, // Callback when validation state changes
@@ -812,6 +816,7 @@ HTMLEditor.defaultProps = {
812
816
  onTagSelect: null,
813
817
  onContextChange: null,
814
818
  globalActions: null, // Redux actions for API calls
819
+ isLiquidEnabled: false,
815
820
  isFullMode: true, // Default to full mode
816
821
  onErrorAcknowledged: null, // Callback when user clicks redirection icon to acknowledge errors
817
822
  onValidationChange: null, // Callback when validation state changes
@@ -229,6 +229,7 @@ const defaultProps = {
229
229
  channel: 'EMAIL',
230
230
  userLocale: 'en',
231
231
  moduleFilterEnabled: true,
232
+ isLiquidEnabled: false,
232
233
  isFullMode: true,
233
234
  onErrorAcknowledged: jest.fn(),
234
235
  onValidationChange: jest.fn(),
@@ -3201,6 +3201,7 @@ describe('HTMLEditor', () => {
3201
3201
  onTagSelect={onTagSelect}
3202
3202
  onContextChange={onContextChange}
3203
3203
  globalActions={globalActions}
3204
+ isLiquidEnabled={true}
3204
3205
  isFullMode={false}
3205
3206
  onErrorAcknowledged={onErrorAcknowledged}
3206
3207
  onValidationChange={onValidationChange}
@@ -3260,6 +3261,20 @@ describe('HTMLEditor', () => {
3260
3261
  expect(screen.getByTestId('device-toggle')).toBeInTheDocument();
3261
3262
  });
3262
3263
 
3264
+ it('should handle isLiquidEnabled prop', () => {
3265
+ render(
3266
+ <TestWrapper>
3267
+ <HTMLEditor isLiquidEnabled={true} />
3268
+ </TestWrapper>
3269
+ );
3270
+
3271
+ act(() => {
3272
+ jest.runAllTimers();
3273
+ });
3274
+
3275
+ expect(screen.getByTestId('editor-toolbar')).toBeInTheDocument();
3276
+ });
3277
+
3263
3278
  it('should handle isFullMode prop', () => {
3264
3279
  render(
3265
3280
  <TestWrapper>
@@ -69,7 +69,7 @@ const CodeEditorPaneComponent = ({
69
69
  }) => {
70
70
  const context = useEditorContext();
71
71
  const {
72
- content, validation, variant,
72
+ content, validation, variant, isLiquidEnabled,
73
73
  } = context || {};
74
74
  const { content: contentValue, updateContent } = content || {};
75
75
  const editorRef = useRef(null);
@@ -298,6 +298,7 @@ const CodeEditorPaneComponent = ({
298
298
  <ValidationErrorDisplay
299
299
  validation={validation}
300
300
  onErrorClick={onErrorClick}
301
+ isLiquidEnabled={isLiquidEnabled}
301
302
  className="code-editor-pane__validation"
302
303
  />
303
304
  </div>
@@ -2,9 +2,11 @@ export const expectedStateGetLiquidTagsRequest = {
2
2
  fetchingLiquidTags: true,
3
3
  fetchingSchema: true,
4
4
  fetchingSchemaError: "",
5
+ liquidTags: [],
5
6
  messages: [],
6
7
  metaEntities: {
7
8
  layouts: [],
9
+ tagLookupMap: {},
8
10
  tags: [],
9
11
  },
10
12
  orgID: "",
@@ -15,9 +17,11 @@ export const expectedStateGetLiquidTagsFailure = {
15
17
  fetchingLiquidTags: false,
16
18
  fetchingSchema: true,
17
19
  fetchingSchemaError: "",
20
+ liquidTags: [],
18
21
  messages: [],
19
22
  metaEntities: {
20
23
  layouts: [],
24
+ tagLookupMap: {},
21
25
  tags: [],
22
26
  },
23
27
  orgID: "",
@@ -28,9 +32,11 @@ export const expectedStateGetLiquidTagsSuccess = {
28
32
  fetchingLiquidTags: false,
29
33
  fetchingSchema: true,
30
34
  fetchingSchemaError: "",
35
+ liquidTags: [],
31
36
  messages: [],
32
37
  metaEntities: {
33
38
  layouts: [],
39
+ tagLookupMap: {},
34
40
  tags: [],
35
41
  },
36
42
  orgID: "",
@@ -41,9 +47,11 @@ export const expectedStateGetSchemaForEntitySuccessTAG = {
41
47
  fetchingLiquidTags: false,
42
48
  fetchingSchema: false,
43
49
  fetchingSchemaError: false,
50
+ liquidTags: [],
44
51
  messages: [],
45
52
  metaEntities: {
46
53
  layouts: undefined,
54
+ tagLookupMap: { undefined: { definition: {} } },
47
55
  tags: { standard: { random: "32" } },
48
56
  },
49
57
  orgID: "",
@@ -54,9 +62,11 @@ export const expectedStateGetSchemaForEntitySuccess = {
54
62
  fetchingLiquidTags: false,
55
63
  fetchingSchema: false,
56
64
  fetchingSchemaError: false,
65
+ liquidTags: [],
57
66
  messages: [],
58
67
  metaEntities: {
59
68
  layouts: undefined,
69
+ tagLookupMap: undefined,
60
70
  tags: undefined,
61
71
  },
62
72
  orgID: "",
@@ -68,9 +78,13 @@ export const expectedForwardedTags = {
68
78
  fetchingSchema: false,
69
79
  fetchingSchemaError: '',
70
80
  injectedTags: undefined,
81
+ liquidTags: [],
71
82
  messages: [],
72
83
  metaEntities: {
73
84
  layouts: [],
85
+ tagLookupMap: {
86
+
87
+ },
74
88
  tags: [],
75
89
  },
76
90
  orgID: "",
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Created by vivek on 22/5/17.
3
3
  */
4
- import { fromJS } from 'immutable';
4
+ import { fromJS, Map as ImmutableMap } from 'immutable';
5
5
  import _ from 'lodash';
6
6
  import * as types from './constants';
7
7
  import initialState from '../../initialState';
8
8
  import { FAILURE } from '../App/constants';
9
+ import { TAG } from '../Whatsapp/constants';
10
+ import { getTagMapValue, getForwardedMapValues, getLoyaltyTagsMapValue } from '../../utils/tagValidations';
9
11
 
10
12
  function capReducer(state = fromJS(initialState.cap), action) {
11
13
  switch (action.type) {
@@ -96,6 +98,39 @@ function capReducer(state = fromJS(initialState.cap), action) {
96
98
  return state
97
99
  .set('fetchingLiquidTags', false);
98
100
  case types.GET_SCHEMA_FOR_ENTITY_SUCCESS: {
101
+ //Process standard tags
102
+ const standardTagMapInitial = _.keyBy(
103
+ action?.data?.metaEntities?.standard,
104
+ item => item?.definition?.value
105
+ );
106
+ // Mapping only the `definition` object instead of the entire item, to reduce space used
107
+ const standardTagMap = _.mapValues(standardTagMapInitial, item => ({
108
+ definition: item?.definition ?? {},
109
+ }));
110
+
111
+ // Process custom tags
112
+ const customSubtags = getTagMapValue(action?.data?.metaEntities?.custom)
113
+ // Process extended tags
114
+ const extendedSubtags = getTagMapValue(action?.data?.metaEntities?.extended);
115
+
116
+ const loyaltySubTagsData = getLoyaltyTagsMapValue(action?.data?.metaEntities?.loyaltyTags);
117
+
118
+ const getExistingTagLookupMap = (state) => {
119
+ if (!state || !state.get) return {};
120
+ const tagLookupMap = state.getIn(['metaEntities', 'tagLookupMap']);
121
+ return state.get('metaEntities') && ImmutableMap.isMap(tagLookupMap)
122
+ ? tagLookupMap.toJS()
123
+ : {};
124
+ };
125
+
126
+ // Combine all maps
127
+ const combinedTagMap = {
128
+ ...standardTagMap,
129
+ ...customSubtags,
130
+ ...extendedSubtags,
131
+ ...loyaltySubTagsData,
132
+ ...getExistingTagLookupMap(state),
133
+ };
99
134
  const stateMeta = state.get("metaEntities");
100
135
  return state
101
136
  .set('fetchingSchema', false)
@@ -103,6 +138,7 @@ function capReducer(state = fromJS(initialState.cap), action) {
103
138
  .set('metaEntities', {
104
139
  layouts: action.data && action.entityType === 'LAYOUT' ? action.data.metaEntities : stateMeta?.layouts,
105
140
  tags: action.data && action.entityType === 'TAG' ? action.data.metaEntities : stateMeta?.tags,
141
+ tagLookupMap: action?.data && action?.entityType === TAG ? combinedTagMap : stateMeta?.tagLookupMap,
106
142
  })
107
143
  .set('fetchingSchemaError', false);
108
144
  }
@@ -110,6 +146,7 @@ function capReducer(state = fromJS(initialState.cap), action) {
110
146
  return state.set('metaEntities', {
111
147
  layouts: [],
112
148
  tags: [],
149
+ tagLookupMap: {},
113
150
  });
114
151
  // eslint-disable-next-line no-case-declarations
115
152
  case types.HIDE_TAGS:
@@ -117,8 +154,23 @@ function capReducer(state = fromJS(initialState.cap), action) {
117
154
  metaEntities.tags.standard = _.filter(state.get('metaEntities').tags.standard, (tag) => action.tagList.indexOf(tag.definition.value) === -1);
118
155
  metaEntities.tags.custom = _.filter(state.get('metaEntities').tags.custom, (tag) => action.tagList.indexOf(tag.name) === -1);
119
156
  return state.setIn(['metaEntities'], metaEntities);
120
- case types.SET_INJECTED_TAGS:
121
- return state.set("injectedTags", action.injectedTags);
157
+ case types.SET_INJECTED_TAGS:
158
+
159
+ // Deep clone the tagLookupMap to avoid direct mutations
160
+ let updatedMetaEntitiesTagLookUp = _.cloneDeep(state.getIn(['metaEntities', 'tagLookupMap']));
161
+ const formattedInjectedTags = getForwardedMapValues(action?.injectedTags);
162
+ // Merge the injectedTags with the existing tagLookupMap
163
+ updatedMetaEntitiesTagLookUp = {
164
+ ...formattedInjectedTags || {},
165
+ ...updatedMetaEntitiesTagLookUp || {},
166
+ };
167
+ return state.set("injectedTags", action.injectedTags).setIn(
168
+ ["metaEntities"],
169
+ fromJS({
170
+ ...state.get("metaEntities"),
171
+ tagLookupMap: updatedMetaEntitiesTagLookUp
172
+ })
173
+ );
122
174
  case types.GET_TOPBAR_MENU_DATA_REQUEST:
123
175
  return state.set('topbarMenuData', fromJS({ status: 'request' }));
124
176
  case types.GET_TOPBAR_MENU_DATA_SUCCESS:
@@ -21,6 +21,7 @@ import {
21
21
  expectedStateGetSchemaForEntitySuccessTAG,
22
22
  expectedStateGetSchemaForEntitySuccess,
23
23
  } from '../mockData';
24
+ import { TAG } from '../../Whatsapp/constants';
24
25
  import { loadItem } from '../../../services/localStorageApi';
25
26
 
26
27
 
@@ -117,3 +118,104 @@ describe('should handle GET_SUPPORT_VIDEOS_CONFIG', () => {
117
118
  expect(reducer(mockedInitialState, action).toJS())?.fetchingSchema?.toEqual(true);
118
119
  });
119
120
  });
121
+
122
+ describe('GET_SCHEMA_FOR_ENTITY_SUCCESS handler', () => {
123
+ it.concurrent('should handle existing tagLookupMap correctly when metaEntities and tagLookupMap exist', () => {
124
+ const initialStateTest = fromJS({
125
+ token: loadItem('token') || '',
126
+ orgID: loadItem('orgID') || '',
127
+ messages: [],
128
+ metaEntities: {
129
+ tags: [{ id: 1, name: 'tag1' }],
130
+ layouts: ['layout1', 'layout2'],
131
+ tagLookupMap: {
132
+ existingTag: { definition: { value: 'existing' } },
133
+ anotherTag: { definition: { value: 'another' } },
134
+ },
135
+ standard: [],
136
+ },
137
+ liquidTags: [],
138
+ fetchingLiquidTags: false,
139
+ fetchingSchema: false,
140
+ fetchingSchemaError: '',
141
+ });
142
+
143
+ let action = {
144
+ type: GET_SCHEMA_FOR_ENTITY_SUCCESS,
145
+ entityType: TAG,
146
+ data: {
147
+ metaEntities: {
148
+ standard: [],
149
+ custom: [],
150
+ extended: [],
151
+ },
152
+ },
153
+ };
154
+
155
+ let newState = reducer(initialStateTest, action);
156
+ let metaEntities = newState.get('metaEntities');
157
+
158
+ expect(metaEntities).toEqual(expect.objectContaining({
159
+ tagLookupMap: expect.objectContaining({
160
+ existingTag: expect.objectContaining({
161
+ definition: expect.objectContaining({
162
+ value: 'existing',
163
+ }),
164
+ }),
165
+ }),
166
+ }));
167
+
168
+ action = {
169
+ type: GET_SCHEMA_FOR_ENTITY_SUCCESS,
170
+ entityType: 'LAYOUT',
171
+ data: {
172
+ metaEntities: {
173
+ layouts: ['layout1', 'layout2'],
174
+ standard: [],
175
+ },
176
+ },
177
+ };
178
+ newState = reducer(initialStateTest, action);
179
+ metaEntities = newState.get('metaEntities');
180
+ expect(metaEntities).toEqual(expect.objectContaining({
181
+ layouts: expect.objectContaining({
182
+ layouts: expect.arrayContaining(['layout1', 'layout2']),
183
+ }),
184
+ }));
185
+ });
186
+
187
+ it.concurrent('should handle non-existent tagLookupMap by returning empty object', () => {
188
+ const initialStateTest = fromJS({
189
+ token: loadItem('token') || '',
190
+ orgID: loadItem('orgID') || '',
191
+ messages: [],
192
+ metaEntities: {
193
+ tagLookupMap: {},
194
+ },
195
+ liquidTags: [],
196
+ fetchingLiquidTags: false,
197
+ fetchingSchema: false,
198
+ fetchingSchemaError: '',
199
+ });
200
+
201
+ const action = {
202
+ type: GET_SCHEMA_FOR_ENTITY_SUCCESS,
203
+ entityType: TAG,
204
+ data: {
205
+ metaEntities: {
206
+ standard: [],
207
+ custom: [],
208
+ extended: [],
209
+ },
210
+ },
211
+ };
212
+
213
+ const newState = reducer(initialStateTest, action);
214
+ const metaEntities = newState.get('metaEntities');
215
+
216
+ // Updated assertions to handle plain object
217
+ expect(metaEntities).toBeDefined();
218
+ expect(metaEntities.tagLookupMap).toBeDefined();
219
+ expect(metaEntities.tagLookupMap).toEqual({});
220
+ });
221
+ });
@@ -173,6 +173,9 @@ export function SlideBoxContent(props) {
173
173
  showTestAndPreviewSlidebox,
174
174
  handleTestAndPreview,
175
175
  handleCloseTestAndPreview,
176
+ restrictPersonalization = false,
177
+ isAnonymousType = false,
178
+ onPersonalizationTokensChange,
176
179
  isTestAndPreviewMode,
177
180
  onHtmlEditorValidationStateChange,
178
181
  } = props;
@@ -637,6 +640,8 @@ export function SlideBoxContent(props) {
637
640
  smsRegister={smsRegister}
638
641
  onShowTemplates={onShowTemplates}
639
642
  eventContextTags={eventContextTags}
643
+ restrictPersonalization={restrictPersonalization}
644
+ isAnonymousType={isAnonymousType}
640
645
  />
641
646
  )}
642
647
 
@@ -677,6 +682,8 @@ export function SlideBoxContent(props) {
677
682
  isTestAndPreviewMode={isTestAndPreviewMode}
678
683
  location={location}
679
684
  onHtmlEditorValidationStateChange={onHtmlEditorValidationStateChange}
685
+ restrictPersonalization={restrictPersonalization}
686
+ isAnonymousType={isAnonymousType}
680
687
  />
681
688
  )}
682
689
  {(isEditEmailWithId || isEmailEditWithContent) && (
@@ -797,6 +804,9 @@ export function SlideBoxContent(props) {
797
804
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
798
805
  handleTestAndPreview={handleTestAndPreview}
799
806
  handleCloseTestAndPreview={handleCloseTestAndPreview}
807
+ restrictPersonalization={restrictPersonalization}
808
+ isAnonymousType={isAnonymousType}
809
+ onPersonalizationTokensChange={onPersonalizationTokensChange}
800
810
  />
801
811
  ) : (
802
812
  <MobilePushNew
@@ -824,6 +834,9 @@ export function SlideBoxContent(props) {
824
834
  eventContextTags={eventContextTags}
825
835
  showLiquidErrorInFooter={showLiquidErrorInFooter}
826
836
  handleClose={handleClose}
837
+ restrictPersonalization={restrictPersonalization}
838
+ isAnonymousType={isAnonymousType}
839
+ onPersonalizationTokensChange={onPersonalizationTokensChange}
827
840
  />
828
841
  )
829
842
  )}
@@ -861,6 +874,9 @@ export function SlideBoxContent(props) {
861
874
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
862
875
  handleTestAndPreview={handleTestAndPreview}
863
876
  handleCloseTestAndPreview={handleCloseTestAndPreview}
877
+ restrictPersonalization={restrictPersonalization}
878
+ isAnonymousType={isAnonymousType}
879
+ onPersonalizationTokensChange={onPersonalizationTokensChange}
864
880
  />
865
881
  ) : (
866
882
  <MobilePushNew
@@ -895,6 +911,8 @@ export function SlideBoxContent(props) {
895
911
  showLiquidErrorInFooter={showLiquidErrorInFooter}
896
912
  onCreateComplete={onCreateComplete}
897
913
  creativesMode={creativesMode}
914
+ restrictPersonalization={restrictPersonalization}
915
+ isAnonymousType={isAnonymousType}
898
916
  />
899
917
  )
900
918
  )}
@@ -1143,6 +1161,8 @@ export function SlideBoxContent(props) {
1143
1161
  supportedTags={supportedTags}
1144
1162
  selectedOfferDetails={selectedOfferDetails}
1145
1163
  eventContextTags={eventContextTags}
1164
+ restrictPersonalization={restrictPersonalization}
1165
+ isAnonymousType={isAnonymousType}
1146
1166
  />
1147
1167
  )}
1148
1168
  {isCreateRcs && (<Rcs
@@ -9,6 +9,7 @@ import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
9
9
  import { PREVIEW } from './constants';
10
10
  import { EMAIL_CREATE_MODES } from '../EmailWrapper/constants';
11
11
  import { hasSupportCKEditor } from '../../utils/common';
12
+ import { getMessageForDevice, getTitleForDevice } from '../../utils/commonUtils';
12
13
 
13
14
  function getFullModeSaveBtn(slidBoxContent, isCreatingTemplate) {
14
15
  if (isCreatingTemplate) {
@@ -43,13 +44,15 @@ function SlideBoxFooter(props) {
43
44
  currentChannel,
44
45
  emailCreateMode,
45
46
  selectedEmailCreateMode,
47
+ restrictPersonalization = false,
48
+ isAnonymousType = false,
49
+ templateData = {},
50
+ hasPersonalizationTokenError: hasPersonalizationTokenErrorProp = false,
46
51
  } = props;
47
-
48
52
  // Calculate if buttons should be disabled
49
53
  // Only apply validation state checks for EMAIL channel in HTML Editor mode (not BEE/DragDrop)
50
54
  // For other channels, BEE editor, or when htmlEditorValidationState is not provided, don't disable based on validation
51
55
  const isEmailChannel = currentChannel?.toUpperCase() === 'EMAIL';
52
- const isSmsChannel = currentChannel?.toUpperCase() === 'SMS';
53
56
  const isEditMode = slidBoxContent === 'editTemplate';
54
57
 
55
58
  // Use selectedEmailCreateMode for accurate mode detection in create mode (emailCreateMode is mapped for backwards compatibility)
@@ -126,9 +129,33 @@ function SlideBoxFooter(props) {
126
129
  const isBEEEditorModeInCreate = !isHTMLEditorMode && !isEditMode;
127
130
  const isBEEEditorMode = isBEEEditorModeInEdit || isBEEEditorModeInCreate;
128
131
  const hasBEEEditorErrors = isEmailChannel && isBEEEditorMode && (hasStandardErrors || hasLiquidErrors) && (!htmlEditorValidationState || !htmlEditorHasErrors);
129
- const hasSmsValidationErrors = isSmsChannel && hasStandardErrors;
130
132
 
131
- const shouldShowErrorInfoNote = hasBEEEditorErrors || hasSmsValidationErrors || isSupportCKEditor;
133
+ const shouldShowErrorInfoNote = hasBEEEditorErrors || isSupportCKEditor;
134
+
135
+ // Check for personalization tokens in title/message when anonymous user tries to save
136
+ const hasPersonalizationTokens = () => {
137
+ if (!isAnonymousType || !templateData) return false;
138
+
139
+ const androidTitle = getTitleForDevice(templateData, 'ANDROID') || '';
140
+ const iosTitle = getTitleForDevice(templateData, 'IOS') || '';
141
+ const androidMessageBody = getMessageForDevice(templateData, 'ANDROID') || '';
142
+ const iosMessageBody = getMessageForDevice(templateData, 'IOS') || '';
143
+ // Check for personalization tags {{ }} in title or message body
144
+ const contentToCheck = `${androidTitle} ${iosTitle} ${androidMessageBody} ${iosMessageBody}`;
145
+ // Check for liquid tags {{ }}
146
+ if (contentToCheck.includes('{{') && contentToCheck.includes('}}')) {
147
+ return true;
148
+ }
149
+ // Check for event context tags [
150
+ if (contentToCheck.includes('[') && contentToCheck.includes(']')) {
151
+ return true;
152
+ }
153
+ return false;
154
+ };
155
+
156
+ // Use prop from parent (FormBuilder flow) when available; else fall back to templateData check
157
+ const hasPersonalizationTokenError = hasPersonalizationTokenErrorProp === true || (restrictPersonalization && hasPersonalizationTokens());
158
+
132
159
  return (
133
160
  <div className="template-footer-width">
134
161
  {shouldShowErrorInfoNote && (
@@ -149,7 +176,7 @@ function SlideBoxFooter(props) {
149
176
  <CapRow>
150
177
  <CapButton
151
178
  onClick={onSave}
152
- disabled={isTemplateNameEmpty || fetchingCmsData || shouldDisableButtons}
179
+ disabled={isTemplateNameEmpty || fetchingCmsData || shouldDisableButtons || hasPersonalizationTokenError}
153
180
  >
154
181
  {isFullMode ? (
155
182
  getFullModeSaveBtn(slidBoxContent, isCreatingTemplate)
@@ -161,7 +188,7 @@ function SlideBoxFooter(props) {
161
188
  <CapButton
162
189
  type="secondary"
163
190
  onClick={onTestAndPreview}
164
- disabled={shouldDisableButtons}
191
+ disabled={shouldDisableButtons || hasPersonalizationTokenError}
165
192
  style={{ marginLeft: '8px' }}
166
193
  >
167
194
  <FormattedMessage {...messages.testAndPreview} />
@@ -222,6 +249,11 @@ SlideBoxFooter.propTypes = {
222
249
  currentChannel: PropTypes.string,
223
250
  emailCreateMode: PropTypes.string,
224
251
  selectedEmailCreateMode: PropTypes.string,
252
+ restrictPersonalization: PropTypes.bool,
253
+ isAnonymousType: PropTypes.bool,
254
+ templateData: PropTypes.object,
255
+ formData: PropTypes.array,
256
+ hasPersonalizationTokenError: PropTypes.bool,
225
257
  };
226
258
 
227
259
  SlideBoxFooter.defaultProps = {
@@ -247,5 +279,7 @@ SlideBoxFooter.defaultProps = {
247
279
  currentChannel: '',
248
280
  emailCreateMode: '',
249
281
  selectedEmailCreateMode: '',
282
+ formData: [],
283
+ hasPersonalizationTokenError: false,
250
284
  };
251
285
  export default SlideBoxFooter;
@@ -52,3 +52,9 @@ export const GENERIC = "GENERIC";
52
52
  export const LIQUID_ERROR_MSG = "LIQUID_ERROR_MSG";
53
53
  export const STANDARD_ERROR_MSG = "STANDARD_ERROR_MSG";
54
54
  export const COMMON_CHANNELS = ['sms', 'email', 'wechat', 'mobilepush', 'webpush', 'line', 'viber', 'facebook', 'call_task', 'ftp', 'assets'];
55
+ export const MIXED = "MIXED";
56
+ export const VISITOR = "VISITOR";
57
+ export const ALLOWED_CHANNELS_FOR_ANONYMOUS = ['mobilepush', 'webpush'];
58
+ export const ALL_CHANNELS_NEW = [
59
+ 'sms', 'email', 'whatsapp', 'facebook', 'line', 'viber', 'rcs', 'zalo', 'inapp', 'call_task', 'ftp',
60
+ ];