@capillarytech/creatives-library 8.0.200-alpha.0 → 8.0.200

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.
@@ -44,6 +44,7 @@ export const REGISTRATION_CUSTOM_FIELD = 'Registration custom fields';
44
44
  export const GIFT_CARDS = 'GIFT_CARDS';
45
45
  export const PROMO_ENGINE = 'PROMO_ENGINE';
46
46
  export const LIQUID_SUPPORT = 'ENABLE_LIQUID_SUPPORT';
47
+ export const ENABLE_NEW_MPUSH = 'ENABLE_NEW_MPUSH';
47
48
  export const CUSTOM_TAG = 'CustomTagMessage';
48
49
  export const CUSTOMER_EXTENDED_FIELD = 'Customer extended fields';
49
50
  export const EXTENDED_TAG = 'ExtendedTagMessage';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.200-alpha.0",
4
+ "version": "8.0.200",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -21,7 +21,8 @@ import {
21
21
  EMAIL_UNSUBSCRIBE_TAG_MANDATORY,
22
22
  BADGES_ISSUE,
23
23
  ENABLE_WECHAT,
24
- LIQUID_SUPPORT
24
+ LIQUID_SUPPORT,
25
+ ENABLE_NEW_MPUSH
25
26
  } from '../constants/unified';
26
27
  import { apiMessageFormatHandler } from './commonUtils';
27
28
 
@@ -125,6 +126,11 @@ export const isEmailUnsubscribeTagMandatory = Auth.hasFeatureAccess.bind(
125
126
  EMAIL_UNSUBSCRIBE_TAG_MANDATORY,
126
127
  );
127
128
 
129
+ export const hasNewMobilePushFeatureEnabled = Auth.hasFeatureAccess.bind(
130
+ null,
131
+ ENABLE_NEW_MPUSH,
132
+ );
133
+
128
134
  //filtering tags based on scope
129
135
  export const filterTags = (tagsToFilter, tagsList) => tagsList?.filter(
130
136
  (tag) => !tagsToFilter?.includes(tag?.definition?.value)
@@ -62,33 +62,12 @@ const createMobilePushPayload = ({
62
62
  throw new Error(intl.formatMessage(messages.templateNameEmptyError));
63
63
  }
64
64
 
65
- // Validate content - allow single platform if explicitly allowed
66
- const allowSinglePlatform = options?.allowSinglePlatform || false;
67
-
68
- if (!allowSinglePlatform) {
69
- // Normal validation: require all supported platforms to have content
70
- if (isAndroidSupported && (!androidContent?.title || !androidContent?.message)) {
71
- throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'Android' }));
72
- }
73
- if (isIosSupported && (!iosContent?.title || !iosContent?.message)) {
74
- throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'iOS' }));
75
- }
76
- } else {
77
- // Single platform mode: require at least one platform to have content
78
- const hasAndroidContent = isAndroidSupported && androidContent?.title?.trim() && androidContent?.message?.trim();
79
- const hasIosContent = isIosSupported && iosContent?.title?.trim() && iosContent?.message?.trim();
80
-
81
- if (!hasAndroidContent && !hasIosContent) {
82
- throw new Error(intl.formatMessage(messages.singlePlatformContentMissing));
83
- }
84
-
85
- // Validate individual platforms that are supported but have incomplete content
86
- if (isAndroidSupported && !hasAndroidContent && androidContent) {
87
- throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'Android' }));
88
- }
89
- if (isIosSupported && !hasIosContent && iosContent) {
90
- throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'iOS' }));
91
- }
65
+ // Validate content
66
+ if (isAndroidSupported && (!androidContent?.title || !androidContent?.message)) {
67
+ throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'Android' }));
68
+ }
69
+ if (isIosSupported && (!iosContent?.title || !iosContent?.message)) {
70
+ throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'iOS' }));
92
71
  }
93
72
 
94
73
  // Ensure imageSrc has the required properties
@@ -261,59 +261,6 @@ describe('createMobilePushPayload', () => {
261
261
  accountData: unsupportedAccountData,
262
262
  })).not.toThrow();
263
263
  });
264
-
265
- // Single Platform Mode Tests
266
- it('should allow single platform when allowSinglePlatform is true - Android only', () => {
267
- expect(() => callWithIntl({
268
- templateName: 'Test',
269
- androidContent: { title: 'Title', message: 'Message' },
270
- iosContent: null,
271
- accountData: mockAccountData,
272
- options: { allowSinglePlatform: true },
273
- })).not.toThrow();
274
- });
275
-
276
- it('should allow single platform when allowSinglePlatform is true - iOS only', () => {
277
- expect(() => callWithIntl({
278
- templateName: 'Test',
279
- androidContent: null,
280
- iosContent: { title: 'Title', message: 'Message' },
281
- accountData: mockAccountData,
282
- options: { allowSinglePlatform: true },
283
- })).not.toThrow();
284
- });
285
-
286
- it('should throw error when no platforms have content even with allowSinglePlatform', () => {
287
- expect(() => callWithIntl({
288
- templateName: 'Test',
289
- androidContent: null,
290
- iosContent: null,
291
- accountData: mockAccountData,
292
- options: { allowSinglePlatform: true },
293
- })).toThrow(intl.formatMessage(messages.singlePlatformContentMissing));
294
- });
295
-
296
- it('should validate individual platforms in single platform mode - Android incomplete', () => {
297
- // Test case where iOS has valid content but Android has incomplete content
298
- expect(() => callWithIntl({
299
- templateName: 'Test',
300
- androidContent: { title: 'Valid Title' }, // Missing message property
301
- iosContent: { title: 'Valid Title', message: 'Valid Message' }, // Complete content
302
- accountData: mockAccountData,
303
- options: { allowSinglePlatform: true },
304
- })).toThrow(intl.formatMessage(messages.contentValidationError, { platform: 'Android' }));
305
- });
306
-
307
- it('should validate individual platforms in single platform mode - iOS incomplete', () => {
308
- // Test case where Android has valid content but iOS has incomplete content
309
- expect(() => callWithIntl({
310
- templateName: 'Test',
311
- androidContent: { title: 'Valid Title', message: 'Valid Message' }, // Complete content
312
- iosContent: { title: 'Valid Title' }, // Missing message property
313
- accountData: mockAccountData,
314
- options: { allowSinglePlatform: true },
315
- })).toThrow(intl.formatMessage(messages.contentValidationError, { platform: 'iOS' }));
316
- });
317
264
  });
318
265
 
319
266
  // Account Data Validation Tests
@@ -668,7 +668,9 @@ export function SlideBoxContent(props) {
668
668
  />
669
669
  )}
670
670
  {isEditMPush && (
671
- (!isFullMode && isLoyaltyModule) ? (
671
+ (isFullMode && !commonUtil.hasNewMobilePushFeatureEnabled()) ||
672
+ (!isFullMode && isLoyaltyModule) ||
673
+ (!isFullMode && !isLoyaltyModule && !commonUtil.hasNewMobilePushFeatureEnabled()) ? (
672
674
  <MobliPushEdit
673
675
  getFormLibraryData={getFormData}
674
676
  setIsLoadingContent={setIsLoadingContent}
@@ -724,7 +726,9 @@ export function SlideBoxContent(props) {
724
726
  )
725
727
  )}
726
728
  {isCreateMPush && (
727
- (!isFullMode && isLoyaltyModule) ? (
729
+ (isFullMode && !commonUtil.hasNewMobilePushFeatureEnabled()) ||
730
+ (!isFullMode && isLoyaltyModule) ||
731
+ (!isFullMode && !isLoyaltyModule && !commonUtil.hasNewMobilePushFeatureEnabled()) ? (
728
732
  <MobilepushWrapper
729
733
  key="creatives-mobilepush-wrapper"
730
734
  date={new Date().getMilliseconds()}
@@ -700,139 +700,4 @@ describe('Test SlideBoxContent container', () => {
700
700
  expect(renderedComponent).toMatchSnapshot();
701
701
  });
702
702
  });
703
-
704
- // Mobile Push Loyalty Module Tests
705
- describe('Mobile Push with Loyalty Module', () => {
706
- it('Should render MobliPushEdit for loyalty module in library mode (edit)', () => {
707
- renderFunction('MOBILE_PUSH', 'editTemplate', { _id: 'test-id' }, {
708
- isFullMode: false,
709
- isLoyaltyModule: true
710
- });
711
- expect(renderedComponent).toMatchSnapshot();
712
- });
713
-
714
- it('Should render MobilePushNew for loyalty module in full mode (edit)', () => {
715
- renderFunction('MOBILE_PUSH', 'editTemplate', { _id: 'test-id' }, {
716
- isFullMode: true,
717
- isLoyaltyModule: true
718
- });
719
- expect(renderedComponent).toMatchSnapshot();
720
- });
721
-
722
- it('Should render MobilepushWrapper for loyalty module in library mode (create)', () => {
723
- renderFunction('MOBILE_PUSH', 'createTemplate', { mode: 'create' }, {
724
- isFullMode: false,
725
- isLoyaltyModule: true
726
- });
727
- expect(renderedComponent).toMatchSnapshot();
728
- });
729
-
730
- it('Should render MobilePushNew for loyalty module in full mode (create)', () => {
731
- renderFunction('MOBILE_PUSH', 'createTemplate', { mode: 'create' }, {
732
- isFullMode: true,
733
- isLoyaltyModule: true
734
- });
735
- expect(renderedComponent).toMatchSnapshot();
736
- });
737
-
738
- it('Should render MobilePushNew for non-loyalty module in library mode (edit)', () => {
739
- renderFunction('MOBILE_PUSH', 'editTemplate', { _id: 'test-id' }, {
740
- isFullMode: false,
741
- isLoyaltyModule: false
742
- });
743
- expect(renderedComponent).toMatchSnapshot();
744
- });
745
-
746
- it('Should render MobilePushNew for non-loyalty module in library mode (create)', () => {
747
- renderFunction('MOBILE_PUSH', 'createTemplate', { mode: 'create' }, {
748
- isFullMode: false,
749
- isLoyaltyModule: false
750
- });
751
- expect(renderedComponent).toMatchSnapshot();
752
- });
753
- });
754
-
755
- // Additional edge case tests to cover uncovered lines
756
- describe('Edge Cases for Uncovered Lines', () => {
757
- it('Should handle getLineType with LINE channel and empty templateData', () => {
758
- // This should cover lines 72-74 in getLineType function
759
- // LINE channel, not full mode, templateData exists but has no _id, no messageBody, and not isDefault
760
- const emptyLineTemplateData = {
761
- type: 'LINE',
762
- isDefault: false,
763
- // No _id and no versions.base.content.messages[0]
764
- };
765
- renderFunction('LINE', 'editTemplate', emptyLineTemplateData, {
766
- isFullMode: false
767
- });
768
- expect(renderedComponent).toMatchSnapshot();
769
- });
770
-
771
- it('Should handle SMS channel with DLT content processing', () => {
772
- // This should cover line 251 in SMS content processing
773
- // Need isDltEnabled = true and updatedSmsEditor as array
774
- // Mock hasTraiDltFeature to return true
775
- const commonUtils = require('../../../utils/common');
776
- const originalHasTraiDltFeature = commonUtils.hasTraiDltFeature;
777
- commonUtils.hasTraiDltFeature = jest.fn(() => true);
778
-
779
- const smsTemplateData = {
780
- type: 'SMS',
781
- versions: {
782
- base: {
783
- 'updated-sms-editor': ['Line 1', 'Line 2', 'Line 3'], // Array to trigger join()
784
- 'sms-editor': 'Fallback SMS content'
785
- }
786
- }
787
- };
788
- renderFunction('SMS', 'preview', smsTemplateData, {
789
- smsRegister: 'DLT', // Enable DLT for library mode
790
- isFullMode: false
791
- });
792
-
793
- // Restore original function
794
- commonUtils.hasTraiDltFeature = originalHasTraiDltFeature;
795
- expect(renderedComponent).toMatchSnapshot();
796
- });
797
-
798
- it('Should handle LINE channel with video content', () => {
799
- // This should cover lines 314-315 for video detection
800
- // Need a LINE message with video property but no text, previewImageUrl, selectedSticker, baseUrl, template, or contents
801
- const lineTemplateDataWithVideo = {
802
- type: 'LINE',
803
- versions: {
804
- base: {
805
- content: {
806
- messages: [{
807
- video: {
808
- originalContentUrl: 'https://example.com/video.mp4',
809
- externalLink: {
810
- linkUri: 'https://example.com/link'
811
- }
812
- }
813
- // No text, previewImageUrl, selectedSticker, baseUrl, template, or contents
814
- }]
815
- }
816
- }
817
- }
818
- };
819
- renderFunction('LINE', 'preview', lineTemplateDataWithVideo);
820
- expect(renderedComponent).toMatchSnapshot();
821
- });
822
-
823
- it('Should handle default case in getChannelPreviewContent', () => {
824
- // This should cover line 395 - default case return
825
- const unknownChannelData = {
826
- versions: {
827
- base: {
828
- content: {
829
- customField: 'custom content'
830
- }
831
- }
832
- }
833
- };
834
- renderFunction('UNKNOWN_CHANNEL', 'preview', unknownChannelData);
835
- expect(renderedComponent).toMatchSnapshot();
836
- });
837
- });
838
703
  });
@@ -1,138 +1,5 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`Test SlideBoxContent container Edge Cases for Uncovered Lines Should handle LINE channel with video content 1`] = `
4
- <SlideBoxContent__CreativesWrapper>
5
- <InjectIntl(TemplatePreview)
6
- channel="line"
7
- charCounterEnabled={false}
8
- content={
9
- Array [
10
- Object {
11
- "actionUrl": "https://example.com/link",
12
- "baseUrl": undefined,
13
- "imageCarousel": Array [
14
- Object {
15
- "actionLabel": undefined,
16
- "imageUrl": undefined,
17
- },
18
- ],
19
- "messageContent": undefined,
20
- "previewImageUrl": undefined,
21
- "selectedSticker": Object {
22
- "animatedStickerUrl": undefined,
23
- "packageId": undefined,
24
- "stickerId": undefined,
25
- "stickerUrl": undefined,
26
- },
27
- "type": "image_carousel",
28
- "video": "https://example.com/video.mp4",
29
- },
30
- ]
31
- }
32
- showCount={true}
33
- templateData={
34
- Object {
35
- "type": "LINE",
36
- "versions": Object {
37
- "base": Object {
38
- "content": Object {
39
- "messages": Array [
40
- Object {
41
- "video": Object {
42
- "externalLink": Object {
43
- "linkUri": "https://example.com/link",
44
- },
45
- "originalContentUrl": "https://example.com/video.mp4",
46
- },
47
- },
48
- ],
49
- },
50
- },
51
- },
52
- }
53
- }
54
- viberBrandName=""
55
- />
56
- <Connect(Connect(UserIsAuthenticated(InjectIntl(CreativesCommon))))
57
- location={
58
- Object {
59
- "pathname": "/sms/edit",
60
- "query": Object {
61
- "isEditFromCampaigns": undefined,
62
- "module": "library",
63
- "type": "embedded",
64
- },
65
- "search": "",
66
- }
67
- }
68
- onCreateComplete={[MockFunction]}
69
- params={
70
- Object {
71
- "id": undefined,
72
- "mode": undefined,
73
- }
74
- }
75
- route={
76
- Object {
77
- "name": "edit_text",
78
- }
79
- }
80
- templateData={
81
- Object {
82
- "type": "LINE",
83
- "versions": Object {
84
- "base": Object {
85
- "content": Object {
86
- "messages": Array [
87
- Object {
88
- "video": Object {
89
- "externalLink": Object {
90
- "linkUri": "https://example.com/link",
91
- },
92
- "originalContentUrl": "https://example.com/video.mp4",
93
- },
94
- },
95
- ],
96
- },
97
- },
98
- },
99
- }
100
- }
101
- />
102
- </SlideBoxContent__CreativesWrapper>
103
- `;
104
-
105
- exports[`Test SlideBoxContent container Edge Cases for Uncovered Lines Should handle SMS channel with DLT content processing 1`] = `
106
- <SlideBoxContent__CreativesWrapper>
107
- <InjectIntl(TemplatePreview)
108
- channel="sms"
109
- charCounterEnabled={true}
110
- content="Fallback SMS content"
111
- showCount={true}
112
- templateData={
113
- Object {
114
- "type": "SMS",
115
- "versions": Object {
116
- "base": Object {
117
- "sms-editor": "Fallback SMS content",
118
- "updated-sms-editor": Array [
119
- "Line 1",
120
- "Line 2",
121
- "Line 3",
122
- ],
123
- },
124
- },
125
- }
126
- }
127
- viberBrandName=""
128
- />
129
- </SlideBoxContent__CreativesWrapper>
130
- `;
131
-
132
- exports[`Test SlideBoxContent container Edge Cases for Uncovered Lines Should handle default case in getChannelPreviewContent 1`] = `<SlideBoxContent__CreativesWrapper />`;
133
-
134
- exports[`Test SlideBoxContent container Edge Cases for Uncovered Lines Should handle getLineType with LINE channel and empty templateData 1`] = `<SlideBoxContent__CreativesWrapper />`;
135
-
136
3
  exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIFE Should handle isTestAndPreviewMode IIFE in Email edit mode with ID 1`] = `
137
4
  <SlideBoxContent__CreativesWrapper>
138
5
  <ForwardRef
@@ -335,18 +202,6 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
335
202
  </SlideBoxContent__CreativesWrapper>
336
203
  `;
337
204
 
338
- exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilePushNew for loyalty module in full mode (create) 1`] = `<SlideBoxContent__CreativesWrapper />`;
339
-
340
- exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilePushNew for loyalty module in full mode (edit) 1`] = `<SlideBoxContent__CreativesWrapper />`;
341
-
342
- exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilePushNew for non-loyalty module in library mode (create) 1`] = `<SlideBoxContent__CreativesWrapper />`;
343
-
344
- exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilePushNew for non-loyalty module in library mode (edit) 1`] = `<SlideBoxContent__CreativesWrapper />`;
345
-
346
- exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilepushWrapper for loyalty module in library mode (create) 1`] = `<SlideBoxContent__CreativesWrapper />`;
347
-
348
- exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobliPushEdit for loyalty module in library mode (edit) 1`] = `<SlideBoxContent__CreativesWrapper />`;
349
-
350
205
  exports[`Test SlideBoxContent container Should handle isTestAndPreviewMode IIFE implementation correctly 1`] = `
351
206
  <SlideBoxContent__CreativesWrapper>
352
207
  <ForwardRef
@@ -21,7 +21,6 @@ import { intlShape } from "react-intl";
21
21
  import "./index.scss";
22
22
  import { GA } from "@capillarytech/cap-ui-utils";
23
23
  import CapNotification from "@capillarytech/cap-ui-library/CapNotification";
24
- import { CapModal } from "@capillarytech/cap-react-ui-library";
25
24
  import { DAEMON } from "@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes";
26
25
  import globalMessages from "../Cap/messages";
27
26
  import * as actions from "./actions";
@@ -530,9 +529,6 @@ const MobilePushNew = ({
530
529
  STANDARD_ERROR_MSG: {},
531
530
  LIQUID_ERROR_MSG: {},
532
531
  });
533
- // Modal state for single platform confirmation
534
- const [showModal, setShowModal] = useState(false);
535
- const [modalContent, setModalContent] = useState({});
536
532
  const [androidContent, setAndroidContent] = useState(INITIAL_CONTENT);
537
533
 
538
534
  const [iosContent, setIosContent] = useState(INITIAL_CONTENT);
@@ -2134,93 +2130,39 @@ const MobilePushNew = ({
2134
2130
  return panes;
2135
2131
  }, [isAndroidSupported, isIosSupported, renderContentFields, formatMessage]);
2136
2132
 
2137
- // Save button disabled logic: require at least one platform to have data
2138
- const hasAndroidData = isAndroidSupported && androidContent?.title?.trim() && androidContent?.message?.trim();
2139
- const hasIosData = isIosSupported && iosContent?.title?.trim() && iosContent?.message?.trim();
2140
- const hasAnyPlatformData = hasAndroidData || hasIosData;
2141
-
2142
- // Validation checks for save button
2143
- const carouselErrors = Object.values(carouselLinkErrors).some((error) => error !== null && error !== "");
2144
- const carouselValid = isCarouselDataValid();
2145
-
2133
+ // Save button disabled logic: only check enabled platforms
2146
2134
  const isSaveDisabled = (
2147
- !hasAnyPlatformData // At least one supported platform must have data
2148
- || (isFullMode && (!templateName || !templateName.trim())) // Template name required in full mode
2149
- || carouselErrors
2150
- || !carouselValid
2135
+ (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim()))
2136
+ || (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim()))
2137
+ || templateNameError
2138
+ || Object.values(carouselLinkErrors).some((error) => error !== null && error !== "")
2139
+ || !isCarouselDataValid()
2151
2140
  );
2152
2141
 
2153
- // Modal handler functions
2154
- const setModalContentHandler = useCallback((type) => {
2155
- if (type === 'ios') {
2156
- const content = {
2157
- title: formatMessage(messages.alertMessage),
2158
- body: formatMessage(messages.iosTemplateNotConfigured),
2159
- type: 'confirm',
2160
- id: 'ios',
2161
- };
2162
- setModalContent(content);
2163
- setShowModal(true);
2164
- } else if (type === 'android') {
2165
- const content = {
2166
- title: formatMessage(messages.alertMessage),
2167
- body: formatMessage(messages.androidTemplateNotConfigured),
2168
- type: 'confirm',
2169
- id: 'android',
2170
- };
2171
- setModalContent(content);
2172
- setShowModal(true);
2142
+ // Validation in handleSave: only show errors for enabled platforms
2143
+ const handleSave = useCallback(() => {
2144
+ if (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim())) {
2145
+ CapNotification.error({
2146
+ message: formatMessage(messages.androidValidationError),
2147
+ });
2148
+ if (onValidationFail) onValidationFail();
2149
+ return;
2173
2150
  }
2174
- }, [formatMessage]);
2175
-
2176
- const handleCancelModal = useCallback(() => {
2177
- setShowModal(false);
2178
- setModalContent({});
2179
- }, []);
2180
-
2181
- // Internal save function with optional modal validation skip
2182
- const handleSaveInternal = useCallback((skipModalValidation = false) => {
2183
- // Check for single platform data and show modal confirmation (unless skipped)
2184
- if (!skipModalValidation) {
2185
- const androidHasData = androidContent?.title?.trim() && androidContent?.message?.trim();
2186
- const iosHasData = iosContent?.title?.trim() && iosContent?.message?.trim();
2187
-
2188
- // If both platforms are supported but only one has data, show confirmation modal
2189
- if (isAndroidSupported && isIosSupported) {
2190
- if (androidHasData && !iosHasData) {
2191
- setModalContentHandler('ios'); // Show iOS not configured modal
2192
- return;
2193
- }
2194
- if (iosHasData && !androidHasData) {
2195
- setModalContentHandler('android'); // Show Android not configured modal
2196
- return;
2197
- }
2198
- }
2151
+ if (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim())) {
2152
+ CapNotification.error({
2153
+ message: formatMessage(messages.iosValidationError),
2154
+ });
2155
+ if (onValidationFail) onValidationFail();
2156
+ return;
2199
2157
  }
2200
-
2201
- // Only validate platforms that have data or are required
2202
- // If user confirmed via modal, we allow single platform data
2203
- if (!skipModalValidation) {
2204
- // In normal flow, require data for all supported platforms
2205
- if (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim())) {
2206
- CapNotification.error({
2207
- message: formatMessage(messages.androidValidationError),
2208
- });
2209
- if (onValidationFail) onValidationFail();
2210
- return;
2211
- }
2212
- if (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim())) {
2213
- CapNotification.error({
2214
- message: formatMessage(messages.iosValidationError),
2215
- });
2216
- if (onValidationFail) onValidationFail();
2217
- return;
2218
- }
2158
+ if (templateNameError) {
2159
+ CapNotification.error({
2160
+ message: formatMessage(messages.emptyTemplateErrorMessage),
2161
+ });
2162
+ return;
2219
2163
  }
2220
- // Note: In modal-confirmed flow (skipModalValidation = true), we skip platform validation
2221
- // since user has already confirmed they want to save with incomplete platform data
2222
-
2223
- // Template name validation - only required in full mode
2164
+ // Only require templateName in full mode
2165
+ // Use ref to get the latest template name value for validation
2224
2166
  const currentTemplateName = templateNameRef.current || templateName;
2225
2167
  if (isFullMode && !currentTemplateName?.trim()) {
2226
2168
  CapNotification.error({
@@ -2385,8 +2327,6 @@ const MobilePushNew = ({
2385
2327
  }
2386
2328
 
2387
2329
  // Create payload with enabled platform content and intl
2388
- // In modal-confirmed flow, we may have intentionally missing platform data
2389
- const allowSinglePlatform = skipModalValidation;
2390
2330
  const payload = createMobilePushPayloadWithIntl({
2391
2331
  templateName: finalTemplateName,
2392
2332
  androidContent: isAndroidSupported ? processedAndroidContent : undefined,
@@ -2397,7 +2337,6 @@ const MobilePushNew = ({
2397
2337
  sameContent,
2398
2338
  options: {
2399
2339
  mode: params?.mode,
2400
- allowSinglePlatform,
2401
2340
  },
2402
2341
  intl,
2403
2342
  });
@@ -2577,68 +2516,6 @@ const MobilePushNew = ({
2577
2516
  isCarouselDataValid,
2578
2517
  isFullMode,
2579
2518
  templateId,
2580
- activeTab,
2581
- setModalContentHandler,
2582
- ]);
2583
-
2584
- // Public save function (with modal validation)
2585
- const handleSave = useCallback(() => {
2586
- handleSaveInternal(false);
2587
- }, [handleSaveInternal]);
2588
-
2589
- // Save function that skips single platform modal validation
2590
- const handleSaveWithoutModal = useCallback(() => {
2591
- handleSaveInternal(true);
2592
- }, [handleSaveInternal]);
2593
-
2594
- const handleConfirmModal = useCallback(() => {
2595
- setShowModal(false);
2596
- setModalContent({});
2597
- // Proceed with save after modal confirmation - skip single platform validation
2598
- handleSaveWithoutModal();
2599
- }, [handleSaveWithoutModal]);
2600
-
2601
- const liquidMiddleWare = useCallback(() => {
2602
- const onError = ({ standardErrors, liquidErrors }) => {
2603
- setErrorMessage((prev) => ({
2604
- STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
2605
- LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
2606
- }));
2607
- };
2608
- const onSuccess = () => handleSave();
2609
-
2610
- validateMobilePushContent([androidContent, iosContent], {
2611
- currentTab: activeTab === ANDROID ? 1 : 2,
2612
- onError,
2613
- onSuccess,
2614
- getLiquidTags: globalActionsProps.getLiquidTags,
2615
- formatMessage,
2616
- messages: formBuilderMessages,
2617
- tagLookupMap: metaEntities?.tagLookupMap || {},
2618
- eventContextTags: metaEntities?.eventContextTags || [],
2619
- isLiquidFlow: hasLiquidSupportFeature(),
2620
- forwardedTags: {},
2621
- skipTags: (tag) => {
2622
- const skipRegexes = [
2623
- /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
2624
- /unsubscribe\(#[a-zA-Z\d]{6}\)/,
2625
- /Link_to_[a-zA-z]/,
2626
- /SURVEY.*\.TOKEN/,
2627
- /^[A-Za-z].*\([a-zA-Z\d]*\)/,
2628
- ];
2629
- return skipRegexes.some((regex) => regex.test(tag));
2630
- },
2631
- singleTab: getSingleTab(accountData),
2632
- });
2633
- }, [
2634
- androidContent,
2635
- iosContent,
2636
- activeTab,
2637
- globalActionsProps,
2638
- formatMessage,
2639
- metaEntities,
2640
- accountData,
2641
- handleSave,
2642
2519
  ]);
2643
2520
 
2644
2521
  // Helper to sync content between platforms
@@ -2670,9 +2547,7 @@ const MobilePushNew = ({
2670
2547
  setTemplateName(value);
2671
2548
  // Update ref to always have the latest value
2672
2549
  templateNameRef.current = value;
2673
- // Only set error if user has interacted and field is empty
2674
- // In full mode, template name is required only at save time, not during typing
2675
- const isInvalid = isFullMode && value.trim() === "";
2550
+ const isInvalid = !value || value.trim() === "";
2676
2551
  setTemplateNameError(isInvalid);
2677
2552
  if (value && onEnterTemplateName) {
2678
2553
  onEnterTemplateName();
@@ -2680,7 +2555,7 @@ const MobilePushNew = ({
2680
2555
  onRemoveTemplateName();
2681
2556
  }
2682
2557
  },
2683
- [onEnterTemplateName, onRemoveTemplateName, isFullMode]
2558
+ [onEnterTemplateName, onRemoveTemplateName]
2684
2559
  );
2685
2560
 
2686
2561
  // --- Only show template name input in full mode (not library/consumer mode) ---
@@ -2709,6 +2584,47 @@ const MobilePushNew = ({
2709
2584
  [isFullMode, formatMessage, templateName, onTemplateNameChange, templateNameError, params?.id]
2710
2585
  );
2711
2586
 
2587
+ const liquidMiddleWare = useCallback(() => {
2588
+ const onError = ({ standardErrors, liquidErrors }) => {
2589
+ setErrorMessage((prev) => ({
2590
+ STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
2591
+ LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
2592
+ }));
2593
+ };
2594
+ const onSuccess = () => handleSave();
2595
+
2596
+ validateMobilePushContent([androidContent, iosContent], {
2597
+ currentTab: activeTab === ANDROID ? 1 : 2,
2598
+ onError,
2599
+ onSuccess,
2600
+ getLiquidTags: globalActionsProps.getLiquidTags,
2601
+ formatMessage,
2602
+ messages: formBuilderMessages,
2603
+ tagLookupMap: metaEntities?.tagLookupMap || {},
2604
+ eventContextTags: metaEntities?.eventContextTags || [],
2605
+ isLiquidFlow: hasLiquidSupportFeature(),
2606
+ forwardedTags: {},
2607
+ skipTags: (tag) => {
2608
+ const skipRegexes = [
2609
+ /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
2610
+ /unsubscribe\(#[a-zA-Z\d]{6}\)/,
2611
+ /Link_to_[a-zA-z]/,
2612
+ /SURVEY.*\.TOKEN/,
2613
+ /^[A-Za-z].*\([a-zA-Z\d]*\)/,
2614
+ ];
2615
+ return skipRegexes.some((regex) => regex.test(tag));
2616
+ },
2617
+ singleTab: getSingleTab(accountData),
2618
+ });
2619
+ }, [
2620
+ androidContent,
2621
+ iosContent,
2622
+ activeTab,
2623
+ globalActionsProps,
2624
+ formatMessage,
2625
+ metaEntities,
2626
+ accountData,
2627
+ ]);
2712
2628
 
2713
2629
  const isLiquidFlow = hasLiquidSupportFeature();
2714
2630
 
@@ -3029,24 +2945,6 @@ const MobilePushNew = ({
3029
2945
  />
3030
2946
  </CapColumn>
3031
2947
  </CapRow>
3032
-
3033
- {/* Modal for single platform confirmation */}
3034
- <CapModal
3035
- visible={showModal}
3036
- title={modalContent.title}
3037
- onOk={handleConfirmModal}
3038
- onCancel={handleCancelModal}
3039
- footer={[
3040
- <CapButton key="cancel" onClick={handleCancelModal}>
3041
- No
3042
- </CapButton>,
3043
- <CapButton key="confirm" type="primary" onClick={handleConfirmModal}>
3044
- Yes
3045
- </CapButton>,
3046
- ]}
3047
- >
3048
- {modalContent.body}
3049
- </CapModal>
3050
2948
  </CapSpin>
3051
2949
  );
3052
2950
  };
@@ -252,10 +252,6 @@ export default defineMessages({
252
252
  id: `${scope}.contentValidationError`,
253
253
  defaultMessage: '{platform} content must have title and message',
254
254
  },
255
- singlePlatformContentMissing: {
256
- id: `${scope}.singlePlatformContentMissing`,
257
- defaultMessage: 'At least one platform must have title and message',
258
- },
259
255
  // File validation error messages for useUpload.js
260
256
  fileSizeError: {
261
257
  id: `${scope}.fileSizeError`,
@@ -273,17 +269,4 @@ export default defineMessages({
273
269
  id: `${scope}.gifFileTypeError`,
274
270
  defaultMessage: 'Only GIF files are allowed',
275
271
  },
276
- // Modal confirmation messages for single platform data
277
- alertMessage: {
278
- id: `${scope}.alertMessage`,
279
- defaultMessage: 'Alert',
280
- },
281
- androidTemplateNotConfigured: {
282
- id: `${scope}.androidTemplateNotConfigured`,
283
- defaultMessage: 'Android template is not configured. Continue save?',
284
- },
285
- iosTemplateNotConfigured: {
286
- id: `${scope}.iosTemplateNotConfigured`,
287
- defaultMessage: 'IOS template is not configured, Save without IOS template',
288
- },
289
272
  });
@@ -1357,16 +1357,19 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1357
1357
  break;
1358
1358
  }
1359
1359
  case MOBILE_PUSH:
1360
- const mpushData = get(template, 'versions.base', template);
1361
- const androidData = get(mpushData, 'ANDROID') || get(mpushData, 'androidContent');
1362
- const iosData = get(mpushData, 'IOS') || get(mpushData, 'iosContent');
1363
- let mpushListingData = androidData;
1364
- if (isEmpty(androidData) || !androidData?.title) {
1365
- mpushListingData = iosData;
1366
- };
1367
- const { title, message, expandableDetails: { style = '', image, carouselData = [], ctas = [], media = [] } = {} } = mpushListingData || {};
1368
- const {url = ''} = media?.[0] || {};
1369
- templateData.content = (
1360
+ if (!commonUtil.hasNewMobilePushFeatureEnabled()) {
1361
+ templateData.content = template;
1362
+ } else {
1363
+ const mpushData = get(template, 'versions.base', template);
1364
+ const androidData = get(mpushData, 'ANDROID') || get(mpushData, 'androidContent');
1365
+ const iosData = get(mpushData, 'IOS') || get(mpushData, 'iosContent');
1366
+ let mpushListingData = androidData;
1367
+ if (isEmpty(androidData) || !androidData?.title) {
1368
+ mpushListingData = iosData;
1369
+ };
1370
+ const { title, message, expandableDetails: { style = '', image, carouselData = [], ctas = [], media = [] } = {} } = mpushListingData || {};
1371
+ const {url = ''} = media?.[0] || {};
1372
+ templateData.content = (
1370
1373
  <div className='mobilepush-container'>
1371
1374
  <div className="app-header">
1372
1375
  <div className="app-header-left">
@@ -1438,7 +1441,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1438
1441
  )}
1439
1442
  </div>
1440
1443
  );
1441
- templateData.isNewMobilePush = true;
1444
+ templateData.isNewMobilePush = commonUtil.hasNewMobilePushFeatureEnabled();
1445
+ }
1442
1446
  break;
1443
1447
  case INAPP:
1444
1448
  templateData.content = template;