@capillarytech/creatives-library 8.0.125 → 8.0.127-alpha.0

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 (110) hide show
  1. package/config/app.js +6 -0
  2. package/containers/App/constants.js +1 -0
  3. package/index.html +3 -1
  4. package/initialReducer.js +2 -0
  5. package/package.json +1 -1
  6. package/services/api.js +94 -1
  7. package/services/tests/api.test.js +191 -0
  8. package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +8 -3
  9. package/tests/integration/TemplateCreation/api-response.js +5 -0
  10. package/tests/integration/TemplateCreation/msw-handler.js +42 -63
  11. package/utils/common.js +7 -0
  12. package/utils/commonUtils.js +2 -6
  13. package/utils/createPayload.js +272 -0
  14. package/utils/tests/createPayload.test.js +761 -0
  15. package/v2Components/CapImageUpload/index.js +59 -46
  16. package/v2Components/CapInAppCTA/index.js +1 -0
  17. package/v2Components/CapMpushCTA/constants.js +25 -0
  18. package/v2Components/CapMpushCTA/index.js +332 -0
  19. package/v2Components/CapMpushCTA/index.scss +95 -0
  20. package/v2Components/CapMpushCTA/messages.js +89 -0
  21. package/v2Components/CapTagList/index.js +177 -120
  22. package/v2Components/CapVideoUpload/constants.js +3 -0
  23. package/v2Components/CapVideoUpload/index.js +167 -110
  24. package/v2Components/CapVideoUpload/messages.js +16 -0
  25. package/v2Components/Carousel/index.js +15 -13
  26. package/v2Components/CustomerSearchSection/_customerSearch.scss +309 -0
  27. package/v2Components/CustomerSearchSection/constants.js +5 -0
  28. package/v2Components/CustomerSearchSection/index.js +367 -0
  29. package/v2Components/CustomerSearchSection/messages.js +20 -0
  30. package/v2Components/CustomerSearchSection/tests/utils.test.js +334 -0
  31. package/v2Components/CustomerSearchSection/utils.js +49 -0
  32. package/v2Components/ErrorInfoNote/style.scss +1 -0
  33. package/v2Components/MobilePushPreviewV2/index.js +37 -5
  34. package/v2Components/TemplatePreview/_templatePreview.scss +114 -72
  35. package/v2Components/TemplatePreview/assets/images/Android _ With date and time.svg +29 -0
  36. package/v2Components/TemplatePreview/assets/images/android.svg +9 -0
  37. package/v2Components/TemplatePreview/assets/images/iOS _ With date and time.svg +26 -0
  38. package/v2Components/TemplatePreview/assets/images/ios.svg +9 -0
  39. package/v2Components/TemplatePreview/index.js +178 -50
  40. package/v2Components/TemplatePreview/messages.js +4 -0
  41. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +169 -0
  42. package/v2Components/TestAndPreviewSlidebox/LeftPanelContent.js +95 -0
  43. package/v2Components/TestAndPreviewSlidebox/PreviewSection.js +69 -0
  44. package/v2Components/TestAndPreviewSlidebox/SendTestMessage.js +68 -0
  45. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +543 -0
  46. package/v2Components/TestAndPreviewSlidebox/actions.js +67 -0
  47. package/v2Components/TestAndPreviewSlidebox/constants.js +67 -0
  48. package/v2Components/TestAndPreviewSlidebox/index.js +592 -0
  49. package/v2Components/TestAndPreviewSlidebox/messages.js +147 -0
  50. package/v2Components/TestAndPreviewSlidebox/reducer.js +233 -0
  51. package/v2Components/TestAndPreviewSlidebox/sagas.js +258 -0
  52. package/v2Components/TestAndPreviewSlidebox/selectors.js +142 -0
  53. package/v2Components/TestAndPreviewSlidebox/tests/CustomValuesEditor.test.js +425 -0
  54. package/v2Components/TestAndPreviewSlidebox/tests/LeftPanelContent.test.js +400 -0
  55. package/v2Components/TestAndPreviewSlidebox/tests/SendTestMessage.test.js +448 -0
  56. package/v2Components/TestAndPreviewSlidebox/tests/actions.test.js +80 -0
  57. package/v2Components/TestAndPreviewSlidebox/tests/reducer.test.js +367 -0
  58. package/v2Components/TestAndPreviewSlidebox/tests/saga.rtl.test.js +192 -0
  59. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +652 -0
  60. package/v2Components/TestAndPreviewSlidebox/tests/selector.test.js +182 -0
  61. package/v2Containers/CreativesContainer/SlideBoxContent.js +22 -10
  62. package/v2Containers/CreativesContainer/SlideBoxFooter.js +23 -2
  63. package/v2Containers/CreativesContainer/index.js +216 -136
  64. package/v2Containers/CreativesContainer/messages.js +4 -0
  65. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +21 -0
  66. package/v2Containers/Email/index.js +27 -2
  67. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +10 -0
  68. package/v2Containers/EmailWrapper/index.js +6 -0
  69. package/v2Containers/InApp/constants.js +1 -0
  70. package/v2Containers/InApp/index.js +13 -13
  71. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  72. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  73. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  74. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  75. package/v2Containers/MobilePush/Create/index.js +1 -0
  76. package/v2Containers/MobilePush/commonMethods.js +7 -14
  77. package/v2Containers/MobilePushNew/actions.js +116 -0
  78. package/v2Containers/MobilePushNew/components/CtaButtons.js +170 -0
  79. package/v2Containers/MobilePushNew/components/MediaUploaders.js +754 -0
  80. package/v2Containers/MobilePushNew/components/PlatformContentFields.js +279 -0
  81. package/v2Containers/MobilePushNew/components/index.js +5 -0
  82. package/v2Containers/MobilePushNew/components/tests/CtaButtons.test.js +779 -0
  83. package/v2Containers/MobilePushNew/components/tests/MediaUploaders.test.js +2114 -0
  84. package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +343 -0
  85. package/v2Containers/MobilePushNew/constants.js +115 -0
  86. package/v2Containers/MobilePushNew/hooks/tests/usePlatformSync.test.js +1299 -0
  87. package/v2Containers/MobilePushNew/hooks/tests/useUpload.test.js +1223 -0
  88. package/v2Containers/MobilePushNew/hooks/usePlatformSync.js +246 -0
  89. package/v2Containers/MobilePushNew/hooks/useUpload.js +726 -0
  90. package/v2Containers/MobilePushNew/index.js +2280 -0
  91. package/v2Containers/MobilePushNew/index.scss +308 -0
  92. package/v2Containers/MobilePushNew/messages.js +226 -0
  93. package/v2Containers/MobilePushNew/reducer.js +160 -0
  94. package/v2Containers/MobilePushNew/sagas.js +198 -0
  95. package/v2Containers/MobilePushNew/selectors.js +55 -0
  96. package/v2Containers/MobilePushNew/tests/reducer.test.js +741 -0
  97. package/v2Containers/MobilePushNew/tests/sagas.test.js +863 -0
  98. package/v2Containers/MobilePushNew/tests/selectors.test.js +425 -0
  99. package/v2Containers/MobilePushNew/tests/utils.test.js +322 -0
  100. package/v2Containers/MobilePushNew/utils.js +33 -0
  101. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +23 -5
  102. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  103. package/v2Containers/TagList/index.js +56 -10
  104. package/v2Containers/Templates/_templates.scss +101 -1
  105. package/v2Containers/Templates/index.js +147 -35
  106. package/v2Containers/Templates/messages.js +8 -0
  107. package/v2Containers/Templates/sagas.js +2 -0
  108. package/v2Containers/Whatsapp/constants.js +1 -0
  109. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +35 -0
  110. package/v2Containers/Email/tests/index.test.js +0 -35
@@ -0,0 +1,2280 @@
1
+ import React, {
2
+ useCallback, useEffect, useMemo, useState, useRef,
3
+ } from "react";
4
+ import PropTypes from "prop-types";
5
+ import { createStructuredSelector, createSelector } from "reselect";
6
+ import { bindActionCreators } from "redux";
7
+ import {
8
+ injectReducer,
9
+ injectSaga,
10
+ } from "@capillarytech/vulcan-react-sdk/utils";
11
+ import { cloneDeep, get, isEmpty } from "lodash";
12
+ import CapCheckbox from "@capillarytech/cap-ui-library/CapCheckbox";
13
+ import CapInput from "@capillarytech/cap-ui-library/CapInput";
14
+ import CapButton from "@capillarytech/cap-ui-library/CapButton";
15
+ import CapRow from "@capillarytech/cap-ui-library/CapRow";
16
+ import CapSpin from "@capillarytech/cap-ui-library/CapSpin";
17
+ import CapTab from "@capillarytech/cap-ui-library/CapTab";
18
+ import CapIcon from "@capillarytech/cap-ui-library/CapIcon";
19
+ import CapColumn from "@capillarytech/cap-ui-library/CapColumn";
20
+ import { intlShape } from "react-intl";
21
+ import "./index.scss";
22
+ import { GA } from "@capillarytech/cap-ui-utils";
23
+ import CapNotification from "@capillarytech/cap-ui-library/CapNotification";
24
+ import { DAEMON } from "@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes";
25
+ import globalMessages from "../Cap/messages";
26
+ import * as actions from "./actions";
27
+ import {
28
+ MEDIA_TYPES_OPTIONS,
29
+ ANDROID,
30
+ IOS,
31
+ MOBILE_PUSH_CHANNEL,
32
+ INITIAL_CONTENT,
33
+ BIG_PICTURE,
34
+ EXTERNAL_LINK,
35
+ GIF,
36
+ CAROUSEL,
37
+ PRIMARY,
38
+ SECONDARY,
39
+ MANUAL_CAROUSEL,
40
+ TEXT,
41
+ } from "./constants";
42
+ import TemplatePreview from "../../v2Components/TemplatePreview";
43
+ import {
44
+ EMBEDDED,
45
+ IMAGE,
46
+ NONE,
47
+ VIDEO,
48
+ DEFAULT,
49
+ FULL,
50
+ TAG,
51
+ LIBRARY,
52
+ ALL,
53
+ } from "../Whatsapp/constants";
54
+ import * as globalActions from "../Cap/actions";
55
+ import {
56
+ isLoadingMetaEntities,
57
+ makeSelectMetaEntities,
58
+ selectCurrentOrgDetails,
59
+ selectLiquidStateDetails,
60
+ setInjectedTags,
61
+ } from "../Cap/selectors";
62
+ import {
63
+ makeSelectMobilePushNew,
64
+ makeSelectUploadedAssetData,
65
+ makeSelectUploadedAssetData0,
66
+ makeSelectUploadedAssetData1,
67
+ makeSelectUploadAssetSuccess,
68
+ makeSelectCreateError,
69
+ makeSelectGetTemplateDetailsInProgress,
70
+ makeSelectAssetUploading,
71
+ } from "./selectors";
72
+ import withCreatives from "../../hoc/withCreatives";
73
+ import { BIG_TEXT, DEEP_LINK, DEVICE_SUPPORTED } from "../InApp/constants";
74
+ import { v2MobilePushSagas } from "./sagas";
75
+ import { getContent } from "../MobilePush/commonMethods";
76
+ import { getMessageObject } from "../../utils/messageUtils";
77
+ import { gtmPush } from "../../utils/gtmTrackers";
78
+ import { createPayload } from "../../utils/createPayload";
79
+ import mobilePushReducer from "./reducer";
80
+ import { hasLiquidSupportFeature } from "../../utils/common";
81
+ import formBuilderMessages from "../../v2Components/FormBuilder/messages";
82
+ import { validateMobilePushContent } from "../../utils/commonUtils";
83
+ import { getSingleTab } from "../InApp/utils";
84
+ import ErrorInfoNote from "../../v2Components/ErrorInfoNote";
85
+ import usePlatformSync from "./hooks/usePlatformSync";
86
+ import useUpload from "./hooks/useUpload";
87
+ import { validateTags } from "../../utils/tagValidations";
88
+ import { PlatformContentFields } from "./components";
89
+ import { CREATE, EDIT, TRACK_CREATE_MPUSH } from "../App/constants";
90
+ import { validateExternalLink, validateDeepLink } from "./utils";
91
+ import messages from "./messages";
92
+ import { EXTERNAL_URL } from "../CreativesContainer/constants";
93
+
94
+ const MobilePushNew = ({
95
+ isFullMode,
96
+ intl,
97
+ onEnterTemplateName,
98
+ onRemoveTemplateName,
99
+ location,
100
+ selectedOfferDetails,
101
+ getDefaultTags,
102
+ injectedTags,
103
+ params,
104
+ templateData = {},
105
+ accountData,
106
+ editData = {},
107
+ mobilePushActions,
108
+ onValidationFail,
109
+ getFormLibraryData,
110
+ uploadedAssetData,
111
+ uploadedAssetData0,
112
+ uploadedAssetData1,
113
+ uploadAssetSuccess,
114
+ metaEntities,
115
+ supportedTags = [],
116
+ globalActions: globalActionsProps,
117
+ handleClose,
118
+ fetchingLiquidValidation,
119
+ createTemplateError,
120
+ isGetFormData,
121
+ getTemplateDetailsInProgress,
122
+ onCreateComplete,
123
+ }) => {
124
+ const { formatMessage } = intl;
125
+
126
+ // Improved edit mode detection for both full and library modes
127
+ const templateId = params?.id || templateData?._id || templateData?.id;
128
+ const isEditMode = !!templateId;
129
+ const computedCreativesMode = isEditMode || templateData?.type === 'MOBILEPUSH' ? 'edit' : 'create';
130
+
131
+ const [sameContent, setSameContent] = useState(false);
132
+ const [templateName, setTemplateName] = useState("");
133
+ const [spin, setSpin] = useState(false);
134
+ const [activeTab, setActiveTab] = useState(ANDROID);
135
+ const [templateNameError, setTemplateNameError] = useState(false);
136
+ // Replace ctaData with platform-specific state
137
+ const [ctaDataAndroid, setCtaDataAndroid] = useState([]);
138
+ const [ctaDataIos, setCtaDataIos] = useState([]);
139
+ const ctaData = activeTab === ANDROID ? ctaDataAndroid : ctaDataIos;
140
+ const [deepLink, setDeepLink] = useState([]);
141
+ const [primaryButtonAndroid, setPrimaryButtonAndroid] = useState(false);
142
+ const [secondaryButtonAndroid, setSecondaryButtonAndroid] = useState(false);
143
+ const [primaryButtonIos, setPrimaryButtonIos] = useState(false);
144
+ const [secondaryButtonIos, setSecondaryButtonIos] = useState(false);
145
+ const [carouselActiveTabIndex, setCarouselActiveTabIndex] = useState(0);
146
+ const [errorMessage, setErrorMessage] = useState({
147
+ STANDARD_ERROR_MSG: {},
148
+ LIQUID_ERROR_MSG: {},
149
+ });
150
+ const [androidContent, setAndroidContent] = useState(INITIAL_CONTENT);
151
+
152
+ const [iosContent, setIosContent] = useState(INITIAL_CONTENT);
153
+ // Refs to track object references for debugging
154
+ const prevEditDataRef = useRef(null);
155
+ const prevUploadedAssetDataRef = useRef(null);
156
+ const prevVideoDataRef = useRef(null);
157
+
158
+ const videoData = useMemo(
159
+ () => {
160
+ const videoDataId = `vd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
161
+ const newVideoData = {
162
+ ...editData,
163
+ uploadedAssetData0,
164
+ uploadedAssetData1,
165
+ _videoDataId: videoDataId, // Add unique ID for tracking
166
+ };
167
+
168
+ // Store references for next comparison
169
+ prevEditDataRef.current = editData;
170
+ prevUploadedAssetDataRef.current = uploadedAssetData;
171
+ prevVideoDataRef.current = newVideoData;
172
+
173
+ return newVideoData;
174
+ },
175
+ [editData, uploadedAssetData0, uploadedAssetData1]
176
+ );
177
+
178
+ const imageData = useMemo(
179
+ () => {
180
+ // For carousel media type, don't include carousel images in Redux structure
181
+ // since carousel images are stored in component state, not Redux
182
+ const currentContent = activeTab === ANDROID ? androidContent : iosContent;
183
+
184
+ if (currentContent?.mediaType === CAROUSEL) {
185
+ // Return only the base editData and uploadedAssetData for non-carousel assets
186
+ return {
187
+ ...editData,
188
+ uploadedAssetData0,
189
+ uploadedAssetData1,
190
+ };
191
+ }
192
+
193
+ return {
194
+ ...editData,
195
+ uploadedAssetData0,
196
+ uploadedAssetData1,
197
+ };
198
+ },
199
+ [editData, uploadedAssetData0, uploadedAssetData1, activeTab, androidContent, iosContent]
200
+ );
201
+
202
+
203
+ const {
204
+ // States
205
+ androidAssetList,
206
+ iosAssetList,
207
+ mpushVideoSrcAndPreview,
208
+ imageSrc,
209
+ videoState,
210
+ // Upload functions
211
+ uploadMpushAsset,
212
+ setUpdateMpushImageSrc,
213
+ setUpdateMpushVideoSrc,
214
+ updateOnMpushImageReUpload,
215
+ updateOnMpushVideoReUpload,
216
+ // Clear functions
217
+ clearImageDataByMediaType,
218
+ resetUploadStates,
219
+ } = useUpload(
220
+ mobilePushActions,
221
+ editData,
222
+ uploadedAssetData0,
223
+ uploadedAssetData1,
224
+ uploadAssetSuccess,
225
+ sameContent,
226
+ setAndroidContent,
227
+ setIosContent,
228
+ activeTab,
229
+ androidContent,
230
+ iosContent
231
+ );
232
+
233
+ const [androidTitleError, setAndroidTitleError] = useState("");
234
+ const [androidMessageError, setAndroidMessageError] = useState("");
235
+ const [iosTitleError, setIosTitleError] = useState("");
236
+ const [iosMessageError, setIosMessageError] = useState("");
237
+
238
+ const [tags, updateTags] = useState([]);
239
+
240
+ // Ref to track if schema has been fetched to prevent duplicate calls
241
+ const schemaFetched = useRef(false);
242
+
243
+ // Ref to track previous mode to detect mode changes
244
+ const prevModeRef = useRef({ id: params?.id, isFullMode });
245
+
246
+ // Add URL validation state
247
+ const [androidExternalLinkError, setAndroidExternalLinkError] = useState("");
248
+ const [iosExternalLinkError, setIosExternalLinkError] = useState("");
249
+ const [androidDeepLinkError, setAndroidDeepLinkError] = useState("");
250
+ const [iosDeepLinkError, setIosDeepLinkError] = useState("");
251
+ const [carouselLinkErrors, setCarouselLinkErrors] = useState({});
252
+
253
+ // Ref for create timeout fallback
254
+ const createTimeoutRef = useRef(null);
255
+
256
+ // Function to validate carousel data completeness
257
+ const validateCarouselData = useCallback((carouselData = []) => {
258
+ if (!carouselData || carouselData.length === 0) {
259
+ return false;
260
+ }
261
+
262
+ return carouselData.every((card) => {
263
+ // Check if image is uploaded (currently only image media type is supported)
264
+ if (card.mediaType === 'image' && !card.imageUrl) {
265
+ return false;
266
+ }
267
+
268
+ // Check if buttons have proper link data when actionOnClick is true
269
+ if (card.buttons && card.buttons.length > 0) {
270
+ return card.buttons.every((button) => {
271
+ if (button.actionOnClick) {
272
+ // Check if link is provided based on link type
273
+ if (button.linkType === DEEP_LINK && !button.deepLinkValue) {
274
+ return false;
275
+ }
276
+ if (button.linkType === EXTERNAL_LINK && !button.externalLinkValue) {
277
+ return false;
278
+ }
279
+ }
280
+ return true;
281
+ });
282
+ }
283
+
284
+ return true;
285
+ });
286
+ }, []);
287
+
288
+ // Function to check if carousel data is valid for both platforms
289
+ const isCarouselDataValid = useCallback(() => {
290
+ // Check Android carousel data
291
+ const androidCarouselValid = androidContent?.mediaType === CAROUSEL
292
+ ? validateCarouselData(androidContent.carouselData)
293
+ : true;
294
+
295
+ // Check iOS carousel data (only if not using same content)
296
+ let iosCarouselValid = true;
297
+ if (sameContent) {
298
+ iosCarouselValid = androidCarouselValid; // If same content, iOS uses same data as Android
299
+ } else if (iosContent?.mediaType === CAROUSEL) {
300
+ iosCarouselValid = validateCarouselData(iosContent.carouselData);
301
+ }
302
+
303
+ return androidCarouselValid && iosCarouselValid;
304
+ }, [androidContent?.mediaType, androidContent?.carouselData, iosContent?.mediaType, iosContent?.carouselData, sameContent, validateCarouselData]);
305
+
306
+ // Define resetFormData early to avoid initialization errors
307
+ const resetFormData = useCallback(() => {
308
+ setTemplateName("");
309
+ setAndroidContent({
310
+ title: "",
311
+ message: "",
312
+ mediaType: NONE,
313
+ buttons: [],
314
+ actionOnClick: false,
315
+ linkType: DEEP_LINK,
316
+ deepLinkValue: "",
317
+ externalLinkValue: "",
318
+ carouselData: [],
319
+ });
320
+ setIosContent({
321
+ title: "",
322
+ message: "",
323
+ mediaType: NONE,
324
+ buttons: [],
325
+ actionOnClick: false,
326
+ linkType: DEEP_LINK,
327
+ deepLinkValue: "",
328
+ externalLinkValue: "",
329
+ carouselData: [],
330
+ });
331
+ // Update resetFormData to reset both ctaDataAndroid and ctaDataIos
332
+ setCtaDataAndroid([]);
333
+ setCtaDataIos([]);
334
+ setPrimaryButtonAndroid(false);
335
+ setSecondaryButtonAndroid(false);
336
+ setPrimaryButtonIos(false);
337
+ setSecondaryButtonIos(false);
338
+ setSameContent(false);
339
+ setTemplateNameError(false);
340
+ setAndroidTitleError("");
341
+ setAndroidMessageError("");
342
+ setIosTitleError("");
343
+ setIosMessageError("");
344
+ setAndroidExternalLinkError("");
345
+ setIosExternalLinkError("");
346
+ setAndroidDeepLinkError("");
347
+ setIosDeepLinkError("");
348
+ setCarouselLinkErrors({});
349
+ resetUploadStates();
350
+ }, [resetUploadStates]);
351
+
352
+ useEffect(() => {
353
+ if (createTemplateError) {
354
+ CapNotification.error({
355
+ message: createTemplateError,
356
+ key: "createMpushError",
357
+ });
358
+ return;
359
+ }
360
+
361
+ // Prevent duplicate schema fetching
362
+ if (schemaFetched.current) return;
363
+
364
+ const { type, module } = location?.query || {};
365
+ const isEmbedded = type === EMBEDDED;
366
+ const context = isEmbedded ? module : DEFAULT;
367
+ const embedded = isEmbedded ? type : FULL;
368
+ const query = {
369
+ layout: 'mobilepush',
370
+ type: TAG,
371
+ context,
372
+ embedded,
373
+ };
374
+ if (getDefaultTags) {
375
+ query.context = getDefaultTags;
376
+ }
377
+ globalActionsProps.fetchSchemaForEntity(query);
378
+ schemaFetched.current = true;
379
+ }, [createTemplateError, location?.query?.type, location?.query?.module, getDefaultTags]);
380
+
381
+ useEffect(() => {
382
+ let tag = get(metaEntities, `tags.standard`, []);
383
+ const { type, module } = location?.query || {};
384
+ if (type === EMBEDDED && module === LIBRARY && !getDefaultTags) {
385
+ tag = supportedTags;
386
+ }
387
+ updateTags(tag);
388
+ }, [metaEntities, location, getDefaultTags, supportedTags]);
389
+
390
+ const templateDescErrorHandler = useCallback(
391
+ (value) => {
392
+ let errorTemplateDescMessage = "";
393
+
394
+ const { unsupportedTags, isBraceError } = validateTags({
395
+ content: value,
396
+ tagsParam: tags,
397
+ injectedTagsParams: injectedTags,
398
+ location,
399
+ tagModule: getDefaultTags,
400
+ }) || {};
401
+ if (value === "" && MEDIA_TYPES_OPTIONS[0].value) {
402
+ errorTemplateDescMessage = formatMessage(
403
+ messages.emptyTemplateDescErrorMessage
404
+ );
405
+ }
406
+ if (unsupportedTags?.length > 0) {
407
+ errorTemplateDescMessage = formatMessage(
408
+ globalMessages.unsupportedTagsValidationError,
409
+ {
410
+ unsupportedTags,
411
+ }
412
+ );
413
+ }
414
+ if (isBraceError) {
415
+ errorTemplateDescMessage = formatMessage(
416
+ globalMessages.unbalanacedCurlyBraces
417
+ );
418
+ }
419
+ return errorTemplateDescMessage;
420
+ },
421
+ [tags, injectedTags, location, getDefaultTags, formatMessage]
422
+ );
423
+
424
+ const validateTitle = useCallback(
425
+ (value) => {
426
+ let error = templateDescErrorHandler(value);
427
+ if (!value || value.trim() === "") {
428
+ error = formatMessage(messages.emptyTemplateDescErrorMessage);
429
+ }
430
+ return error || "";
431
+ },
432
+ [templateDescErrorHandler, formatMessage]
433
+ );
434
+
435
+ const validateMessage = useCallback(
436
+ (value) => {
437
+ let error = templateDescErrorHandler(value);
438
+ if (!value || value.trim() === "") {
439
+ error = formatMessage(messages.emptyTemplateDescErrorMessage);
440
+ }
441
+ return error || "";
442
+ },
443
+ [templateDescErrorHandler, formatMessage]
444
+ );
445
+
446
+ const handleOnTagsContextChange = useCallback(
447
+ (data) => {
448
+ const { type } = location?.query || {};
449
+ const tempData = (data || "").toLowerCase();
450
+ const isEmbedded = type === EMBEDDED;
451
+ const embedded = isEmbedded ? type : FULL;
452
+ const context = tempData === ALL ? DEFAULT : tempData;
453
+ const query = {
454
+ layout: 'mobilepush',
455
+ type: TAG,
456
+ context,
457
+ embedded,
458
+ };
459
+ globalActionsProps.fetchSchemaForEntity(query);
460
+ },
461
+ [globalActionsProps, location]
462
+ );
463
+
464
+ useEffect(() => {
465
+ // accountData IS the selectedWeChatAccount object based on mapStateToProps
466
+ const accountObj = accountData || {};
467
+ const deepLinkObj = accountObj?.configs?.deeplink || "";
468
+
469
+ if (!isEmpty(accountObj)) {
470
+ const { configs = {} } = accountObj;
471
+ const isAndroidSupported = configs?.android === DEVICE_SUPPORTED;
472
+
473
+ try {
474
+ const parsedDeepLinks = JSON.parse(deepLinkObj || "{}");
475
+ const deepLinkKeys = Object.values(parsedDeepLinks);
476
+ const keys = deepLinkKeys?.map((link) => ({
477
+ label: link?.name,
478
+ value: link?.link,
479
+ title: link?.link,
480
+ }));
481
+
482
+ setActiveTab(isAndroidSupported ? ANDROID : IOS);
483
+ setDeepLink(keys);
484
+ } catch (error) {
485
+ console.error("[MobilePushNew] Error parsing deeplinks:", error, { deepLinkObj });
486
+ setDeepLink([]);
487
+ }
488
+ }
489
+ }, [accountData?.configs?.deeplink, accountData?.configs?.android]);
490
+
491
+ // Add debug logs for template switching and data population
492
+ useEffect(() => {
493
+ resetFormData();
494
+ if (params?.id) {
495
+ setSpin(true);
496
+ mobilePushActions.getTemplateDetails(params.id);
497
+ }
498
+ // No need to resetStore here, as we want to keep the new template's details
499
+ }, [params?.id, resetFormData, mobilePushActions]);
500
+
501
+ // Add a useEffect to reset form data only when switching from create mode to edit mode
502
+ useEffect(() => {
503
+ const currentMode = { id: params?.id, isFullMode };
504
+ const prevMode = prevModeRef.current;
505
+ const isEditModeNow = !!(params?.id && isFullMode);
506
+ const wasCreateMode = !(prevMode.id && prevMode.isFullMode);
507
+ const isModeChange = prevMode.id !== currentMode.id || prevMode.isFullMode !== currentMode.isFullMode;
508
+
509
+ // Only reset if we're switching from create mode to edit mode
510
+ if (isModeChange && isEditModeNow && wasCreateMode) {
511
+ resetFormData();
512
+ }
513
+
514
+ prevModeRef.current = currentMode;
515
+ }, [params?.id, isFullMode, resetFormData]);
516
+
517
+ // Add a useEffect to reset dataPopulated when switching templates
518
+ useEffect(() => {
519
+ // setDataPopulated(false); // Removed
520
+ }, [params?.id]);
521
+
522
+ // Data population useEffect - ONLY for edit mode
523
+ useEffect(() => {
524
+ if (!isEditMode) {
525
+ return;
526
+ }
527
+
528
+ // Do NOT reset form state here; only populate data
529
+ // resetFormData();
530
+
531
+ const { name = "", versions = {} } = editData?.templateDetails || {};
532
+ const editContent = versions?.base || {};
533
+
534
+ // If templateDetails exist but content is empty/invalid, hide spinner
535
+ if (editData?.templateDetails && isEmpty(editContent)) {
536
+ setSpin(false);
537
+ return;
538
+ }
539
+
540
+ if (editContent && !isEmpty(editContent)) {
541
+ // Populate data immediately
542
+ setTemplateName(name);
543
+ // Process Android content
544
+ const androidContentType = editContent?.ANDROID;
545
+ if (!isEmpty(androidContentType)) {
546
+ const {
547
+ title: androidTitle = "",
548
+ message: androidMessage = "",
549
+ ctas: androidCta = [],
550
+ expandableDetails: androidExpandableDetails = {},
551
+ image: androidImage = "",
552
+ cta: androidMainCta = null, // This is the main notification body CTA
553
+ } = androidContentType || {};
554
+
555
+ // Only destructure androidExpandableDetails here
556
+ const { style: androidStyle, ctas: androidCtas = [], image: androidExpandableImage = "" } = androidExpandableDetails || {};
557
+ // In iOS content extraction, use unique names:
558
+
559
+ // Determine media type based on style and available content
560
+ let androidMediaType = NONE;
561
+ let androidImageSrc = "";
562
+ let androidVideoSrc = "";
563
+ let androidVideoPreview = "";
564
+ let carouselDataFromEdit = [];
565
+
566
+ if (androidStyle === BIG_PICTURE) {
567
+ androidMediaType = IMAGE;
568
+ androidImageSrc = androidExpandableImage || androidImage;
569
+ } else if (androidStyle === BIG_TEXT) {
570
+ androidMediaType = NONE;
571
+ } else if (androidStyle === MANUAL_CAROUSEL) {
572
+ androidMediaType = CAROUSEL;
573
+ }
574
+
575
+ // Handle video/GIF content from expandableDetails.media array (new format)
576
+ if (androidExpandableDetails?.media && Array.isArray(androidExpandableDetails.media) && androidExpandableDetails.media.length > 0) {
577
+ const mediaItem = androidExpandableDetails.media[0];
578
+ if (mediaItem.type === VIDEO) {
579
+ // Distinguish between actual video and GIF based on URL extension
580
+ if (mediaItem.url && mediaItem.url.toLowerCase().includes('.gif')) {
581
+ androidMediaType = GIF;
582
+ } else {
583
+ androidMediaType = VIDEO;
584
+ }
585
+ androidVideoSrc = mediaItem.url;
586
+ androidVideoPreview = mediaItem.url;
587
+ } else if (mediaItem.type === GIF) {
588
+ androidMediaType = GIF;
589
+ androidVideoSrc = mediaItem.url;
590
+ androidVideoPreview = mediaItem.url;
591
+ }
592
+ }
593
+ // Handle video content if present in image field (legacy format)
594
+ else if (androidImage && (androidImage.includes('.mp4') || androidImage.includes('.mov'))) {
595
+ androidMediaType = VIDEO;
596
+ androidVideoSrc = androidImage;
597
+ androidVideoPreview = androidImage; // Use same for preview
598
+ }
599
+ // Handle GIF content if present in image field (legacy format)
600
+ else if (androidImage && androidImage.toLowerCase().includes('.gif')) {
601
+ androidMediaType = GIF;
602
+ androidVideoSrc = androidImage;
603
+ androidVideoPreview = androidImage; // Use same for preview
604
+ }
605
+ // Handle carousel content from expandableDetails.carouselData
606
+ else if ((androidExpandableDetails?.style === MANUAL_CAROUSEL || androidContentType?.type === CAROUSEL) && androidExpandableDetails?.carouselData) {
607
+ androidMediaType = CAROUSEL;
608
+ // Process carousel data from editData
609
+ carouselDataFromEdit = androidExpandableDetails.carouselData.map((card) => ({
610
+ mediaType: card.mediaType || IMAGE.toLowerCase(),
611
+ imageUrl: card.imageUrl || '',
612
+ videoSrc: card.videoSrc || '',
613
+ buttons: card.buttons || [],
614
+ }));
615
+
616
+ // Initialize carousel images in Redux state for proper display in uploaders
617
+ if (carouselDataFromEdit.length > 0) {
618
+ const firstCard = carouselDataFromEdit[0];
619
+ if (firstCard.mediaType === 'image' && firstCard.imageUrl) {
620
+ // Set the first carousel image in Redux state for the uploader to recognize
621
+ setUpdateMpushImageSrc(firstCard.imageUrl, 0, CAROUSEL);
622
+ }
623
+ }
624
+ }
625
+
626
+ let androidButtonsValue = [];
627
+ if (Array.isArray(androidCtas)) {
628
+ androidButtonsValue = androidCtas;
629
+ } else if (Array.isArray(androidCta)) {
630
+ androidButtonsValue = androidCta;
631
+ }
632
+ const androidContentData = {
633
+ title: androidTitle,
634
+ message: androidMessage,
635
+ mediaType: androidMediaType,
636
+ buttons: androidButtonsValue,
637
+ actionOnClick: false,
638
+ linkType: DEEP_LINK,
639
+ deepLinkValue: "",
640
+ externalLinkValue: "",
641
+ imageSrc: androidImageSrc,
642
+ videoSrc: androidVideoSrc,
643
+ videoPreview: androidVideoPreview,
644
+ carouselData: carouselDataFromEdit, // Initialize carousel data
645
+ };
646
+
647
+ setAndroidContent(androidContentData);
648
+ // Set image/video sources for upload state with platform isolation
649
+ if (androidImageSrc) {
650
+ setUpdateMpushImageSrc(androidImageSrc, 0, IMAGE);
651
+ }
652
+
653
+ // Apply exact same simple pattern as images for videos/GIFs
654
+ if (androidVideoSrc) {
655
+ setUpdateMpushVideoSrc(0, {
656
+ videoSrc: androidVideoSrc,
657
+ previewUrl: androidVideoPreview,
658
+ }, true); // isInitialization = true
659
+ }
660
+
661
+ // Check for main notification body CTA (actionOnClick functionality)
662
+ if (androidMainCta?.actionLink) {
663
+ androidContentData.actionOnClick = true;
664
+
665
+ // Determine link type based on saved CTA type (prioritize saved type over URL pattern)
666
+ if (androidMainCta.type === EXTERNAL_URL) {
667
+ androidContentData.linkType = EXTERNAL_LINK;
668
+ androidContentData.externalLinkValue = androidMainCta.actionLink;
669
+ androidContentData.deepLinkValue = "";
670
+ } else if (androidMainCta.type === DEEP_LINK) {
671
+ androidContentData.linkType = DEEP_LINK;
672
+ androidContentData.deepLinkValue = androidMainCta.actionLink;
673
+ androidContentData.externalLinkValue = "";
674
+ } else if (androidMainCta.actionLink.startsWith('http://') || androidMainCta.actionLink.startsWith('https://')) {
675
+ androidContentData.linkType = EXTERNAL_LINK;
676
+ androidContentData.externalLinkValue = androidMainCta.actionLink;
677
+ androidContentData.deepLinkValue = "";
678
+ } else {
679
+ androidContentData.linkType = DEEP_LINK;
680
+ androidContentData.deepLinkValue = androidMainCta.actionLink;
681
+ androidContentData.externalLinkValue = "";
682
+ }
683
+ } else {
684
+ // If no CTA data, ensure both link values are empty
685
+ androidContentData.deepLinkValue = "";
686
+ androidContentData.externalLinkValue = "";
687
+ }
688
+
689
+ // Set CTA data for buttons (from expandableDetails.ctas)
690
+ const androidButtons = androidExpandableDetails?.ctas || [];
691
+ if (androidButtons.length > 0) {
692
+ const ctaDataFromAndroid = androidButtons.map((button, index) => ({
693
+ text: button.actionText || button.label || "",
694
+ index,
695
+ url: button.actionLink || "",
696
+ urlType: button.type || DEEP_LINK,
697
+ ctaType: index === 0 ? PRIMARY : SECONDARY,
698
+ isSaved: true, // Mark as saved since it's loaded from backend
699
+ }));
700
+ setCtaDataAndroid(ctaDataFromAndroid);
701
+ setPrimaryButtonAndroid(androidButtons.length >= 1);
702
+ setSecondaryButtonAndroid(androidButtons.length >= 2);
703
+ } else {
704
+ setCtaDataAndroid([]);
705
+ setPrimaryButtonAndroid(false);
706
+ setSecondaryButtonAndroid(false);
707
+ }
708
+ }
709
+
710
+ // Process iOS content
711
+ const iosContentType = editContent?.IOS;
712
+ if (!isEmpty(iosContentType)) {
713
+ const {
714
+ title: iosTitle = "",
715
+ message: iosMessage = "",
716
+ ctas = [],
717
+ expandableDetails: iosExpandableDetails = {},
718
+ image: iosImage = "",
719
+ // Removed unused iosCta and iosMainCta variables
720
+ } = iosContentType || {};
721
+
722
+ // Only destructure iosExpandableDetails here
723
+ const { style: iosStyle, image: iosExpandableImage = "" } = iosExpandableDetails || {};
724
+ // In iOS content extraction, use unique names:
725
+
726
+ // Determine media type based on style and available content
727
+ let iosMediaType = NONE;
728
+ let iosImageSrc = "";
729
+ let iosVideoSrc = "";
730
+ let iosVideoPreview = "";
731
+ let carouselDataFromEdit = [];
732
+
733
+ if (iosStyle === BIG_PICTURE) {
734
+ iosMediaType = IMAGE;
735
+ iosImageSrc = iosExpandableImage || iosImage;
736
+ } else if (iosStyle === BIG_TEXT) {
737
+ iosMediaType = NONE;
738
+ } else if (iosStyle === MANUAL_CAROUSEL) {
739
+ iosMediaType = CAROUSEL;
740
+ }
741
+
742
+ // Handle video/GIF content from expandableDetails.media array (new format)
743
+ if (iosExpandableDetails?.media && Array.isArray(iosExpandableDetails.media) && iosExpandableDetails.media.length > 0) {
744
+ const mediaItem = iosExpandableDetails.media[0];
745
+ if (mediaItem.type === VIDEO) {
746
+ // Distinguish between actual video and GIF based on URL extension
747
+ if (mediaItem.url && mediaItem.url.toLowerCase().includes('.gif')) {
748
+ iosMediaType = GIF;
749
+ } else {
750
+ iosMediaType = VIDEO;
751
+ }
752
+ iosVideoSrc = mediaItem.url;
753
+ iosVideoPreview = mediaItem.url;
754
+ } else if (mediaItem.type === GIF) {
755
+ iosMediaType = GIF;
756
+ iosVideoSrc = mediaItem.url;
757
+ iosVideoPreview = mediaItem.url;
758
+ }
759
+ }
760
+ // Handle video content if present in image field (legacy format)
761
+ else if (iosImage && (iosImage.includes('.mp4') || iosImage.includes('.mov'))) {
762
+ iosMediaType = VIDEO;
763
+ iosVideoSrc = iosImage;
764
+ iosVideoPreview = iosImage; // Use same for preview
765
+ }
766
+ // Handle GIF content if present in image field (legacy format)
767
+ else if (iosImage && iosImage.toLowerCase().includes('.gif')) {
768
+ iosMediaType = GIF;
769
+ iosVideoSrc = iosImage;
770
+ iosVideoPreview = iosImage; // Use same for preview
771
+ }
772
+ // Handle carousel content from expandableDetails.carouselData
773
+ else if ((iosExpandableDetails?.style === MANUAL_CAROUSEL || iosContentType?.type === CAROUSEL) && iosExpandableDetails?.carouselData) {
774
+ iosMediaType = CAROUSEL;
775
+ // Process carousel data from editData
776
+ carouselDataFromEdit = iosExpandableDetails.carouselData.map((card) => ({
777
+ mediaType: card.mediaType || IMAGE.toLowerCase(),
778
+ imageUrl: card.imageUrl || '',
779
+ videoSrc: card.videoSrc || '',
780
+ buttons: card.buttons || [],
781
+ }));
782
+
783
+ // Initialize carousel images in Redux state for proper display in uploaders
784
+ if (carouselDataFromEdit.length > 0) {
785
+ const firstCard = carouselDataFromEdit[0];
786
+ if (firstCard.mediaType === 'image' && firstCard.imageUrl) {
787
+ // Set the first carousel image in Redux state for the uploader to recognize
788
+ setUpdateMpushImageSrc(firstCard.imageUrl, 1, CAROUSEL);
789
+ }
790
+ }
791
+ }
792
+
793
+ let iosButtonsValue = [];
794
+ if (Array.isArray(ctas)) {
795
+ iosButtonsValue = ctas;
796
+ }
797
+ const iosContentData = {
798
+ title: iosTitle,
799
+ message: iosMessage,
800
+ mediaType: iosMediaType,
801
+ buttons: iosButtonsValue,
802
+ actionOnClick: false,
803
+ linkType: DEEP_LINK,
804
+ deepLinkValue: "",
805
+ externalLinkValue: "",
806
+ imageSrc: iosImageSrc,
807
+ videoSrc: iosVideoSrc,
808
+ videoPreview: iosVideoPreview,
809
+ carouselData: carouselDataFromEdit, // Initialize carousel data
810
+ };
811
+
812
+ setIosContent(iosContentData);
813
+ // Set image/video sources for upload state with platform isolation
814
+ if (iosImageSrc) {
815
+ setUpdateMpushImageSrc(iosImageSrc, 1, IMAGE);
816
+ }
817
+
818
+ // Apply exact same simple pattern as images for videos/GIFs
819
+ if (iosVideoSrc) {
820
+ setUpdateMpushVideoSrc(1, {
821
+ videoSrc: iosVideoSrc,
822
+ previewUrl: iosVideoPreview,
823
+ }, true); // isInitialization = true
824
+ }
825
+
826
+ // Check for main notification body CTA (actionOnClick functionality)
827
+ // Removed all iosMainCta references (was not defined or used)
828
+
829
+ // Set CTA data for buttons (from expandableDetails.ctas)
830
+ const iosButtons = iosExpandableDetails?.ctas || [];
831
+ if (iosButtons.length > 0) {
832
+ const ctaDataFromIos = iosButtons.map((button, index) => ({
833
+ text: button.actionText || button.label || "",
834
+ index,
835
+ url: button.actionLink || "",
836
+ urlType: button.type || DEEP_LINK,
837
+ ctaType: index === 0 ? PRIMARY : SECONDARY,
838
+ isSaved: true, // Mark as saved since it's loaded from backend
839
+ }));
840
+ setCtaDataIos(ctaDataFromIos);
841
+ setPrimaryButtonIos(iosButtons.length >= 1);
842
+ setSecondaryButtonIos(iosButtons.length >= 2);
843
+ } else {
844
+ setCtaDataIos([]);
845
+ setPrimaryButtonIos(false);
846
+ setSecondaryButtonIos(false);
847
+ }
848
+ }
849
+
850
+ // Mark initial load as complete after data population and hide spinner
851
+ // setIsInitialLoad(false); // Removed
852
+ // setDataPopulated(true); // Removed
853
+ setSpin(false); // Hide spinner only after all state is populated
854
+ }
855
+ }, [editData?.templateDetails, isEditMode, params?.id]);
856
+
857
+ // Data population useEffect - for library mode (not full mode) using templateData
858
+ useEffect(() => {
859
+ if (isFullMode || !templateData || isEmpty(templateData)) {
860
+ return;
861
+ }
862
+
863
+ // Do NOT reset form state here; only populate data
864
+ // resetFormData();
865
+
866
+ // templateData is expected to have a similar structure as editData.templateDetails
867
+ const { name = "", versions = {} } = templateData || {};
868
+ const templateContent = versions?.base || {};
869
+
870
+ if (isEmpty(templateContent)) {
871
+ setSpin(false);
872
+ return;
873
+ }
874
+
875
+ setTemplateName(name);
876
+ // Process Android content
877
+ const androidContentType = templateContent?.ANDROID;
878
+ if (!isEmpty(androidContentType)) {
879
+ const {
880
+ title: androidTitle = "",
881
+ message: androidMessage = "",
882
+ ctas: androidCta = [],
883
+ expandableDetails: androidExpandableDetails = {},
884
+ image: androidImage = "",
885
+ cta: androidMainCta = null,
886
+ } = androidContentType || {};
887
+ const { style: androidStyle, ctas: androidCtas = [], image: androidExpandableImage = "" } = androidExpandableDetails || {};
888
+ let androidMediaType = NONE;
889
+ let androidImageSrc = "";
890
+ let androidVideoSrc = "";
891
+ let androidVideoPreview = "";
892
+ let carouselDataFromTemplate = [];
893
+ if (androidStyle === BIG_PICTURE) {
894
+ androidMediaType = IMAGE;
895
+ androidImageSrc = androidExpandableImage || androidImage;
896
+ } else if (androidStyle === BIG_TEXT) {
897
+ androidMediaType = NONE;
898
+ } else if (androidStyle === MANUAL_CAROUSEL) {
899
+ androidMediaType = CAROUSEL;
900
+ }
901
+ if (androidExpandableDetails?.media && Array.isArray(androidExpandableDetails.media) && androidExpandableDetails.media.length > 0) {
902
+ const mediaItem = androidExpandableDetails.media[0];
903
+ if (mediaItem.type === VIDEO) {
904
+ if (mediaItem.url && mediaItem.url.toLowerCase().includes('.gif')) {
905
+ androidMediaType = GIF;
906
+ } else {
907
+ androidMediaType = VIDEO;
908
+ }
909
+ androidVideoSrc = mediaItem.url;
910
+ androidVideoPreview = mediaItem.url;
911
+ } else if (mediaItem.type === GIF) {
912
+ androidMediaType = GIF;
913
+ androidVideoSrc = mediaItem.url;
914
+ androidVideoPreview = mediaItem.url;
915
+ }
916
+ } else if (androidImage && (androidImage.includes('.mp4') || androidImage.includes('.mov'))) {
917
+ androidMediaType = VIDEO;
918
+ androidVideoSrc = androidImage;
919
+ androidVideoPreview = androidImage;
920
+ } else if (androidImage && androidImage.toLowerCase().includes('.gif')) {
921
+ androidMediaType = GIF;
922
+ androidVideoSrc = androidImage;
923
+ androidVideoPreview = androidImage;
924
+ } else if ((androidExpandableDetails?.style === MANUAL_CAROUSEL || androidContentType?.type === CAROUSEL) && androidExpandableDetails?.carouselData) {
925
+ androidMediaType = CAROUSEL;
926
+ carouselDataFromTemplate = androidExpandableDetails.carouselData.map((card) => ({
927
+ mediaType: card.mediaType || IMAGE.toLowerCase(),
928
+ imageUrl: card.imageUrl || '',
929
+ videoSrc: card.videoSrc || '',
930
+ buttons: card.buttons || [],
931
+ }));
932
+
933
+ // Initialize carousel images in Redux state for proper display in uploaders
934
+ if (carouselDataFromTemplate.length > 0) {
935
+ const firstCard = carouselDataFromTemplate[0];
936
+ if (firstCard.mediaType === 'image' && firstCard.imageUrl) {
937
+ // Set the first carousel image in Redux state for the uploader to recognize
938
+ setUpdateMpushImageSrc(firstCard.imageUrl, 0, CAROUSEL);
939
+ }
940
+ }
941
+ }
942
+ let androidButtonsValue = [];
943
+ if (Array.isArray(androidCtas)) {
944
+ androidButtonsValue = androidCtas;
945
+ } else if (Array.isArray(androidCta)) {
946
+ androidButtonsValue = androidCta;
947
+ }
948
+ const androidContentData = {
949
+ title: androidTitle,
950
+ message: androidMessage,
951
+ mediaType: androidMediaType,
952
+ buttons: androidButtonsValue,
953
+ actionOnClick: false,
954
+ linkType: DEEP_LINK,
955
+ deepLinkValue: "",
956
+ externalLinkValue: "",
957
+ imageSrc: androidImageSrc,
958
+ videoSrc: androidVideoSrc,
959
+ videoPreview: androidVideoPreview,
960
+ carouselData: carouselDataFromTemplate,
961
+ };
962
+ setAndroidContent(androidContentData);
963
+ if (androidImageSrc) {
964
+ setUpdateMpushImageSrc(androidImageSrc, 0, IMAGE);
965
+ }
966
+ if (androidVideoSrc) {
967
+ setUpdateMpushVideoSrc(0, {
968
+ videoSrc: androidVideoSrc,
969
+ previewUrl: androidVideoPreview,
970
+ }, true);
971
+ }
972
+ if (androidMainCta?.actionLink) {
973
+ androidContentData.actionOnClick = true;
974
+ if (androidMainCta.type === EXTERNAL_URL) {
975
+ androidContentData.linkType = EXTERNAL_LINK;
976
+ androidContentData.externalLinkValue = androidMainCta.actionLink;
977
+ androidContentData.deepLinkValue = "";
978
+ } else if (androidMainCta.type === DEEP_LINK) {
979
+ androidContentData.linkType = DEEP_LINK;
980
+ androidContentData.deepLinkValue = androidMainCta.actionLink;
981
+ androidContentData.externalLinkValue = "";
982
+ } else if (androidMainCta.actionLink.startsWith('http://') || androidMainCta.actionLink.startsWith('https://')) {
983
+ androidContentData.linkType = EXTERNAL_LINK;
984
+ androidContentData.externalLinkValue = androidMainCta.actionLink;
985
+ androidContentData.deepLinkValue = "";
986
+ } else {
987
+ androidContentData.linkType = DEEP_LINK;
988
+ androidContentData.deepLinkValue = androidMainCta.actionLink;
989
+ androidContentData.externalLinkValue = "";
990
+ }
991
+ } else {
992
+ androidContentData.deepLinkValue = "";
993
+ androidContentData.externalLinkValue = "";
994
+ }
995
+ const androidButtons = androidExpandableDetails?.ctas || [];
996
+ if (androidButtons.length > 0) {
997
+ const ctaDataFromAndroid = androidButtons.map((button, index) => ({
998
+ text: button.actionText || button.label || "",
999
+ index,
1000
+ url: button.actionLink || "",
1001
+ urlType: button.type || DEEP_LINK,
1002
+ ctaType: index === 0 ? PRIMARY : SECONDARY,
1003
+ isSaved: true,
1004
+ }));
1005
+ setCtaDataAndroid(ctaDataFromAndroid);
1006
+ setPrimaryButtonAndroid(androidButtons.length >= 1);
1007
+ setSecondaryButtonAndroid(androidButtons.length >= 2);
1008
+ } else {
1009
+ setCtaDataAndroid([]);
1010
+ setPrimaryButtonAndroid(false);
1011
+ setSecondaryButtonAndroid(false);
1012
+ }
1013
+ }
1014
+ // Process iOS content
1015
+ const iosContentType = templateContent?.IOS;
1016
+ if (!isEmpty(iosContentType)) {
1017
+ const {
1018
+ title: iosTitle = "",
1019
+ message: iosMessage = "",
1020
+ ctas = [],
1021
+ expandableDetails: iosExpandableDetails = {},
1022
+ image: iosImage = "",
1023
+ } = iosContentType || {};
1024
+ const { style: iosStyle, image: iosExpandableImage = "" } = iosExpandableDetails || {};
1025
+ let iosMediaType = NONE;
1026
+ let iosImageSrc = "";
1027
+ let iosVideoSrc = "";
1028
+ let iosVideoPreview = "";
1029
+ let carouselDataFromTemplate = [];
1030
+ if (iosStyle === BIG_PICTURE) {
1031
+ iosMediaType = IMAGE;
1032
+ iosImageSrc = iosExpandableImage || iosImage;
1033
+ } else if (iosStyle === BIG_TEXT) {
1034
+ iosMediaType = NONE;
1035
+ } else if (iosStyle === MANUAL_CAROUSEL) {
1036
+ iosMediaType = CAROUSEL;
1037
+ }
1038
+ if (iosExpandableDetails?.media && Array.isArray(iosExpandableDetails.media) && iosExpandableDetails.media.length > 0) {
1039
+ const mediaItem = iosExpandableDetails.media[0];
1040
+ if (mediaItem.type === VIDEO) {
1041
+ if (mediaItem.url && mediaItem.url.toLowerCase().includes('.gif')) {
1042
+ iosMediaType = GIF;
1043
+ } else {
1044
+ iosMediaType = VIDEO;
1045
+ }
1046
+ iosVideoSrc = mediaItem.url;
1047
+ iosVideoPreview = mediaItem.url;
1048
+ } else if (mediaItem.type === GIF) {
1049
+ iosMediaType = GIF;
1050
+ iosVideoSrc = mediaItem.url;
1051
+ iosVideoPreview = mediaItem.url;
1052
+ }
1053
+ } else if (iosImage && (iosImage.includes('.mp4') || iosImage.includes('.mov'))) {
1054
+ iosMediaType = VIDEO;
1055
+ iosVideoSrc = iosImage;
1056
+ iosVideoPreview = iosImage;
1057
+ } else if (iosImage && iosImage.toLowerCase().includes('.gif')) {
1058
+ iosMediaType = GIF;
1059
+ iosVideoSrc = iosImage;
1060
+ iosVideoPreview = iosImage;
1061
+ } else if ((iosExpandableDetails?.style === MANUAL_CAROUSEL || iosContentType?.type === CAROUSEL) && iosExpandableDetails?.carouselData) {
1062
+ iosMediaType = CAROUSEL;
1063
+ carouselDataFromTemplate = iosExpandableDetails.carouselData.map((card) => ({
1064
+ mediaType: card.mediaType || IMAGE.toLowerCase(),
1065
+ imageUrl: card.imageUrl || '',
1066
+ videoSrc: card.videoSrc || '',
1067
+ buttons: card.buttons || [],
1068
+ }));
1069
+
1070
+ // Initialize carousel images in Redux state for proper display in uploaders
1071
+ if (carouselDataFromTemplate.length > 0) {
1072
+ const firstCard = carouselDataFromTemplate[0];
1073
+ if (firstCard.mediaType === 'image' && firstCard.imageUrl) {
1074
+ // Set the first carousel image in Redux state for the uploader to recognize
1075
+ setUpdateMpushImageSrc(firstCard.imageUrl, 1, CAROUSEL);
1076
+ }
1077
+ }
1078
+ }
1079
+ let iosButtonsValue = [];
1080
+ if (Array.isArray(ctas)) {
1081
+ iosButtonsValue = ctas;
1082
+ }
1083
+ const iosContentData = {
1084
+ title: iosTitle,
1085
+ message: iosMessage,
1086
+ mediaType: iosMediaType,
1087
+ buttons: iosButtonsValue,
1088
+ actionOnClick: false,
1089
+ linkType: DEEP_LINK,
1090
+ deepLinkValue: "",
1091
+ externalLinkValue: "",
1092
+ imageSrc: iosImageSrc,
1093
+ videoSrc: iosVideoSrc,
1094
+ videoPreview: iosVideoPreview,
1095
+ carouselData: carouselDataFromTemplate,
1096
+ };
1097
+ setIosContent(iosContentData);
1098
+ if (iosImageSrc) {
1099
+ setUpdateMpushImageSrc(iosImageSrc, 1, IMAGE);
1100
+ }
1101
+ if (iosVideoSrc) {
1102
+ setUpdateMpushVideoSrc(1, {
1103
+ videoSrc: iosVideoSrc,
1104
+ previewUrl: iosVideoPreview,
1105
+ }, true);
1106
+ }
1107
+ const iosButtons = iosExpandableDetails?.ctas || [];
1108
+ if (iosButtons.length > 0) {
1109
+ const ctaDataFromIos = iosButtons.map((button, index) => ({
1110
+ text: button.actionText || button.label || "",
1111
+ index,
1112
+ url: button.actionLink || "",
1113
+ urlType: button.type || DEEP_LINK,
1114
+ ctaType: index === 0 ? PRIMARY : SECONDARY,
1115
+ isSaved: true,
1116
+ }));
1117
+ setCtaDataIos(ctaDataFromIos);
1118
+ setPrimaryButtonIos(iosButtons.length >= 1);
1119
+ setSecondaryButtonIos(iosButtons.length >= 2);
1120
+ } else {
1121
+ setCtaDataIos([]);
1122
+ setPrimaryButtonIos(false);
1123
+ setSecondaryButtonIos(false);
1124
+ }
1125
+ }
1126
+ setSpin(false);
1127
+ }, [isFullMode, templateData]);
1128
+
1129
+ // Determine platform support from accountData
1130
+ const isAndroidSupported = accountData?.configs?.android === '1';
1131
+ const isIosSupported = accountData?.configs?.ios === '1';
1132
+
1133
+ // Validation logic for template creation/update
1134
+ const isAndroidFieldsMissing = isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim());
1135
+ const isIosFieldsMissing = isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim());
1136
+
1137
+ // Add changeSourceRef for debounced sync
1138
+ const changeSourceRef = useRef(null);
1139
+
1140
+ const {
1141
+ handleTitleChange,
1142
+ handleMessageChange,
1143
+ handleTagSelect,
1144
+ handleMediaTypeChange,
1145
+ handleActionOnClickChange,
1146
+ handleLinkTypeChange,
1147
+ } = usePlatformSync({
1148
+ setAndroidContent,
1149
+ setIosContent,
1150
+ validateTitle,
1151
+ validateMessage,
1152
+ setAndroidTitleError,
1153
+ setAndroidMessageError,
1154
+ setIosTitleError,
1155
+ setIosMessageError,
1156
+ androidContent,
1157
+ iosContent,
1158
+ updateOnMpushImageReUpload,
1159
+ updateOnMpushVideoReUpload,
1160
+ sameContent,
1161
+ changeSourceRef,
1162
+ });
1163
+
1164
+ // Platform-specific deeplink and external link handlers
1165
+ const handleDeepLinkChange = useCallback((platform, deepLinkValue) => {
1166
+ // Validate deep link
1167
+ const deepLinkError = validateDeepLink(deepLinkValue, formatMessage, messages);
1168
+
1169
+ if (platform === ANDROID) {
1170
+ setAndroidContent((prev) => {
1171
+ const updated = {
1172
+ ...prev,
1173
+ deepLinkValue,
1174
+ // Clear external link value when deep link is selected
1175
+ externalLinkValue: "",
1176
+ };
1177
+ return updated;
1178
+ });
1179
+ setAndroidDeepLinkError(deepLinkError || "");
1180
+ } else {
1181
+ setIosContent((prev) => {
1182
+ const updated = {
1183
+ ...prev,
1184
+ deepLinkValue,
1185
+ // Clear external link value when deep link is selected
1186
+ externalLinkValue: "",
1187
+ };
1188
+ return updated;
1189
+ });
1190
+ setIosDeepLinkError(deepLinkError || "");
1191
+ }
1192
+ }, [validateDeepLink]);
1193
+
1194
+ const handleExternalLinkChange = useCallback((platform, externalLinkValue) => {
1195
+ // Validate URL
1196
+ const urlError = validateExternalLink(externalLinkValue, formatMessage, messages);
1197
+
1198
+ if (platform === ANDROID) {
1199
+ setAndroidContent((prev) => {
1200
+ const updated = {
1201
+ ...prev,
1202
+ externalLinkValue,
1203
+ // Clear deep link value when external link is entered
1204
+ deepLinkValue: "",
1205
+ };
1206
+ return updated;
1207
+ });
1208
+ setAndroidExternalLinkError(urlError || "");
1209
+ } else {
1210
+ setIosContent((prev) => {
1211
+ const updated = {
1212
+ ...prev,
1213
+ externalLinkValue,
1214
+ // Clear deep link value when external link is entered
1215
+ deepLinkValue: "",
1216
+ };
1217
+ return updated;
1218
+ });
1219
+ setIosExternalLinkError(urlError || "");
1220
+ }
1221
+ }, [validateExternalLink]);
1222
+
1223
+ // Carousel data handler for platform-specific carousel management
1224
+ const updateCarouselLinkError = useCallback((cardIndex, field, error) => {
1225
+ setCarouselLinkErrors((prev) => ({
1226
+ ...prev,
1227
+ [`${cardIndex}-${field}`]: error,
1228
+ }));
1229
+ }, []);
1230
+
1231
+ const handleCarouselDataChange = useCallback((platform, carouselData) => {
1232
+ if (platform === ANDROID) {
1233
+ setAndroidContent((prev) => {
1234
+ const updated = { ...prev, carouselData };
1235
+ return updated;
1236
+ });
1237
+ } else {
1238
+ setIosContent((prev) => {
1239
+ const updated = { ...prev, carouselData };
1240
+ return updated;
1241
+ });
1242
+ }
1243
+ }, []);
1244
+
1245
+ // Update updateHandler to sync button state for both platforms if sameContent is true
1246
+ const updateHandler = useCallback(
1247
+ (ctaDataParam, index) => {
1248
+ if (sameContent) {
1249
+ setCtaDataAndroid((prevState) => {
1250
+ const clonedCta = cloneDeep(prevState);
1251
+ clonedCta[index] = ctaDataParam;
1252
+ return clonedCta;
1253
+ });
1254
+ setCtaDataIos((prevState) => {
1255
+ const clonedCta = cloneDeep(prevState);
1256
+ clonedCta[index] = ctaDataParam;
1257
+ return clonedCta;
1258
+ });
1259
+ // Update button state for both platforms
1260
+ if (index === 0) {
1261
+ setPrimaryButtonAndroid(true);
1262
+ setPrimaryButtonIos(true);
1263
+ } else if (index === 1) {
1264
+ setSecondaryButtonAndroid(true);
1265
+ setSecondaryButtonIos(true);
1266
+ }
1267
+ } else if (activeTab === ANDROID) {
1268
+ setCtaDataAndroid((prevState) => {
1269
+ const clonedCta = cloneDeep(prevState);
1270
+ clonedCta[index] = ctaDataParam;
1271
+ return clonedCta;
1272
+ });
1273
+ if (index === 0) setPrimaryButtonAndroid(true);
1274
+ if (index === 1) setSecondaryButtonAndroid(true);
1275
+ } else {
1276
+ setCtaDataIos((prevState) => {
1277
+ const clonedCta = cloneDeep(prevState);
1278
+ clonedCta[index] = ctaDataParam;
1279
+ return clonedCta;
1280
+ });
1281
+ if (index === 0) setPrimaryButtonIos(true);
1282
+ if (index === 1) setSecondaryButtonIos(true);
1283
+ }
1284
+ },
1285
+ [activeTab, sameContent]
1286
+ );
1287
+
1288
+ // Update deleteHandler to sync button state for both platforms if sameContent is true
1289
+ const deleteHandler = useCallback((index) => {
1290
+ if (sameContent) {
1291
+ setCtaDataAndroid((prevState) => {
1292
+ const clonedCta = cloneDeep(prevState);
1293
+ const filteredCta = clonedCta.filter((cta) => cta.index !== index);
1294
+ return filteredCta;
1295
+ });
1296
+ setCtaDataIos((prevState) => {
1297
+ const clonedCta = cloneDeep(prevState);
1298
+ const filteredCta = clonedCta.filter((cta) => cta.index !== index);
1299
+ return filteredCta;
1300
+ });
1301
+ // Update button state for both platforms
1302
+ if (index === 0) {
1303
+ setPrimaryButtonAndroid(false);
1304
+ setPrimaryButtonIos(false);
1305
+ } else if (index === 1) {
1306
+ setSecondaryButtonAndroid(false);
1307
+ setSecondaryButtonIos(false);
1308
+ }
1309
+ } else if (activeTab === ANDROID) {
1310
+ setCtaDataAndroid((prevState) => {
1311
+ const clonedCta = cloneDeep(prevState);
1312
+ const filteredCta = clonedCta.filter((cta) => cta.index !== index);
1313
+ return filteredCta;
1314
+ });
1315
+ if (index === 0) setPrimaryButtonAndroid(false);
1316
+ if (index === 1) setSecondaryButtonAndroid(false);
1317
+ } else {
1318
+ setCtaDataIos((prevState) => {
1319
+ const clonedCta = cloneDeep(prevState);
1320
+ const filteredCta = clonedCta.filter((cta) => cta.index !== index);
1321
+ return filteredCta;
1322
+ });
1323
+ if (index === 0) setPrimaryButtonIos(false);
1324
+ if (index === 1) setSecondaryButtonIos(false);
1325
+ }
1326
+ }, [activeTab, sameContent]);
1327
+
1328
+ const onTagSelect = useCallback(
1329
+ (data, id) => {
1330
+ const platform = activeTab === ANDROID ? ANDROID : IOS;
1331
+ handleTagSelect(platform, data, id === 0);
1332
+ },
1333
+ [activeTab, handleTagSelect]
1334
+ );
1335
+
1336
+ const renderContentFields = useCallback(
1337
+ (platform) => {
1338
+ const isAndroid = platform === ANDROID;
1339
+ const currentContent = isAndroid ? androidContent : iosContent;
1340
+ // Only show error if the platform is enabled
1341
+ let titleError = "";
1342
+ let messageError = "";
1343
+ let externalLinkError = "";
1344
+ let deepLinkError = "";
1345
+ if (isAndroid) {
1346
+ titleError = isAndroidSupported ? androidTitleError : "";
1347
+ messageError = isAndroidSupported ? androidMessageError : "";
1348
+ externalLinkError = isAndroidSupported ? androidExternalLinkError : "";
1349
+ deepLinkError = isAndroidSupported ? androidDeepLinkError : "";
1350
+ } else {
1351
+ titleError = isIosSupported ? iosTitleError : "";
1352
+ messageError = isIosSupported ? iosMessageError : "";
1353
+ externalLinkError = isIosSupported ? iosExternalLinkError : "";
1354
+ deepLinkError = isIosSupported ? iosDeepLinkError : "";
1355
+ }
1356
+ const primaryButton = isAndroid ? primaryButtonAndroid : primaryButtonIos;
1357
+ const secondaryButton = isAndroid ? secondaryButtonAndroid : secondaryButtonIos;
1358
+ const setPrimaryButton = isAndroid ? setPrimaryButtonAndroid : setPrimaryButtonIos;
1359
+ const setSecondaryButton = isAndroid ? setSecondaryButtonAndroid : setSecondaryButtonIos;
1360
+
1361
+ const handlers = {
1362
+ handleTitleChange,
1363
+ handleMessageChange,
1364
+ handleMediaTypeChange,
1365
+ handleActionOnClickChange,
1366
+ handleLinkTypeChange,
1367
+ handleDeepLinkChange: (value) => handleDeepLinkChange(platform, value),
1368
+ handleExternalLinkChange: (value) => handleExternalLinkChange(platform, value),
1369
+ onTagSelect,
1370
+ handleOnTagsContextChange,
1371
+ };
1372
+
1373
+ const tagListProps = {
1374
+ moduleFilterEnabled: location?.query?.type !== EMBEDDED,
1375
+ label: formatMessage(messages.addLabels),
1376
+ location,
1377
+ tags: tags || [],
1378
+ injectedTags: injectedTags || {},
1379
+ selectedOfferDetails,
1380
+ };
1381
+
1382
+ // Fix nested ternary for videoAssetList and gifAssetList
1383
+ let videoAssetList = {};
1384
+ let gifAssetList = {};
1385
+ if (currentContent?.mediaType === VIDEO) {
1386
+ videoAssetList = platform === ANDROID ? androidAssetList : iosAssetList;
1387
+ }
1388
+ if (currentContent?.mediaType === GIF) {
1389
+ gifAssetList = platform === ANDROID ? androidAssetList : iosAssetList;
1390
+ }
1391
+ const mediaUploaderProps = {
1392
+ mediaType: currentContent?.mediaType,
1393
+ activeTab: platform,
1394
+ imageSrc,
1395
+ uploadMpushAsset,
1396
+ isFullMode,
1397
+ setUpdateMpushImageSrc,
1398
+ updateOnMpushImageReUpload,
1399
+ imageData,
1400
+ videoAssetList,
1401
+ gifAssetList,
1402
+ setUpdateMpushVideoSrc,
1403
+ // Add video content state for fallback when Redux is empty (same pattern as imageSrc)
1404
+ videoSrc: {
1405
+ androidVideoSrc: videoState?.androidVideoSrc || '',
1406
+ iosVideoSrc: videoState?.iosVideoSrc || '',
1407
+ androidVideoPreview: videoState?.androidVideoPreview || '',
1408
+ iosVideoPreview: videoState?.iosVideoPreview || '',
1409
+ },
1410
+ // Create truly platform-specific video data with no cross-platform information
1411
+ videoDataForVideo: currentContent?.mediaType === VIDEO ? {
1412
+ // Keep BOTH platforms available like images do - let MediaUploaders choose which to show
1413
+ uploadedAssetData0: uploadedAssetData0 || {},
1414
+ uploadedAssetData1: uploadedAssetData1 || {},
1415
+ } : {},
1416
+ videoDataForGif: currentContent?.mediaType === GIF ? {
1417
+ // Keep BOTH platforms available like images do - let MediaUploaders choose which to show
1418
+ uploadedAssetData0: uploadedAssetData0 || {},
1419
+ uploadedAssetData1: uploadedAssetData1 || {},
1420
+ } : {},
1421
+ videoData,
1422
+ clearImageDataByMediaType,
1423
+ carouselData: currentContent?.carouselData || [],
1424
+ onCarouselDataChange: handleCarouselDataChange,
1425
+ mobilePushActions,
1426
+ carouselActiveTabIndex,
1427
+ setCarouselActiveTabIndex,
1428
+ carouselLinkErrors,
1429
+ updateCarouselLinkError,
1430
+ linkProps: { deepLink },
1431
+ videoPreviewKey: videoState?.videoPreviewKey,
1432
+ };
1433
+
1434
+ const ctaButtonProps = {
1435
+ primaryButton,
1436
+ secondaryButton,
1437
+ setPrimaryButton,
1438
+ setSecondaryButton,
1439
+ ctaData,
1440
+ updateHandler,
1441
+ deleteHandler,
1442
+ deepLink,
1443
+ };
1444
+
1445
+ const linkProps = {
1446
+ deepLink,
1447
+ deepLinkValue: currentContent?.deepLinkValue || "",
1448
+ externalLinkValue: currentContent?.externalLinkValue || "",
1449
+ };
1450
+
1451
+ return (
1452
+ <PlatformContentFields
1453
+ key={`platform-fields-${platform}`}
1454
+ deviceType={platform}
1455
+ content={currentContent}
1456
+ errors={{
1457
+ title: titleError,
1458
+ message: messageError,
1459
+ externalLink: externalLinkError,
1460
+ deepLink: deepLinkError,
1461
+ }}
1462
+ handlers={handlers}
1463
+ tagListProps={tagListProps}
1464
+ mediaUploaderProps={mediaUploaderProps}
1465
+ ctaButtonProps={ctaButtonProps}
1466
+ linkProps={linkProps}
1467
+ sameContent={sameContent}
1468
+ formatMessage={formatMessage}
1469
+ />
1470
+ );
1471
+ }, [androidContent, iosContent, androidTitleError, iosTitleError, androidMessageError, iosMessageError, androidExternalLinkError, iosExternalLinkError, androidDeepLinkError, iosDeepLinkError, 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, handleExternalLinkChange, onTagSelect, handleOnTagsContextChange, setUpdateMpushImageSrc, updateOnMpushImageReUpload, setUpdateMpushVideoSrc, updateOnMpushVideoReUpload, clearImageDataByMediaType, handleCarouselDataChange, updateCarouselLinkError, sameContent, updateHandler, deleteHandler]
1472
+ );
1473
+
1474
+ // PANES: Only render enabled platforms
1475
+ const PANES = useMemo(() => {
1476
+ const panes = [];
1477
+ if (isAndroidSupported) {
1478
+ panes.push({
1479
+ key: ANDROID,
1480
+ tab: (
1481
+ <CapRow>
1482
+ <CapIcon type="android" />
1483
+ {formatMessage(messages.android)}
1484
+ </CapRow>
1485
+ ),
1486
+ content: renderContentFields(ANDROID),
1487
+ });
1488
+ }
1489
+ if (isIosSupported) {
1490
+ panes.push({
1491
+ key: IOS,
1492
+ tab: (
1493
+ <CapRow>
1494
+ <CapIcon type="ios" />
1495
+ {formatMessage(messages.ios)}
1496
+ </CapRow>
1497
+ ),
1498
+ content: renderContentFields(IOS),
1499
+ });
1500
+ }
1501
+ return panes;
1502
+ }, [isAndroidSupported, isIosSupported, renderContentFields, formatMessage]);
1503
+
1504
+ // Save button disabled logic: only check enabled platforms
1505
+ const isSaveDisabled = (
1506
+ (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim()))
1507
+ || (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim()))
1508
+ || templateNameError
1509
+ || Object.values(carouselLinkErrors).some((error) => error !== null && error !== "")
1510
+ || !isCarouselDataValid()
1511
+ );
1512
+
1513
+ // Validation in handleSave: only show errors for enabled platforms
1514
+ const handleSave = useCallback(() => {
1515
+ if (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim())) {
1516
+ CapNotification.error({
1517
+ message: formatMessage(messages.androidValidationError),
1518
+ });
1519
+ if (onValidationFail) onValidationFail();
1520
+ return;
1521
+ }
1522
+ if (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim())) {
1523
+ CapNotification.error({
1524
+ message: formatMessage(messages.iosValidationError),
1525
+ });
1526
+ if (onValidationFail) onValidationFail();
1527
+ return;
1528
+ }
1529
+ if (templateNameError) {
1530
+ CapNotification.error({
1531
+ message: formatMessage(messages.emptyTemplateErrorMessage),
1532
+ });
1533
+ return;
1534
+ }
1535
+ // Only require templateName in full mode
1536
+ if (isFullMode && !templateName?.trim()) {
1537
+ CapNotification.error({
1538
+ message: formatMessage(messages.emptyTemplateErrorMessage),
1539
+ });
1540
+ return;
1541
+ }
1542
+
1543
+ // In library mode, set templateName programmatically if empty
1544
+ let finalTemplateName = templateName;
1545
+ if (!isFullMode && (!finalTemplateName || !finalTemplateName.trim())) {
1546
+ if (androidContent?.title && androidContent.title.trim()) {
1547
+ finalTemplateName = androidContent.title.trim();
1548
+ } else if (iosContent?.title && iosContent.title.trim()) {
1549
+ finalTemplateName = iosContent.title.trim();
1550
+ } else {
1551
+ finalTemplateName = '';
1552
+ }
1553
+ }
1554
+
1555
+ // Validate external links
1556
+ const androidUrlError = validateExternalLink(androidContent.externalLinkValue, formatMessage, messages);
1557
+ const iosUrlError = validateExternalLink(iosContent.externalLinkValue, formatMessage, messages);
1558
+
1559
+ setAndroidExternalLinkError(androidUrlError || "");
1560
+ setIosExternalLinkError(iosUrlError || "");
1561
+
1562
+ // Validate deep links
1563
+ const androidDeepLinkUrlError = validateDeepLink(androidContent.deepLinkValue, formatMessage, messages);
1564
+ const iosDeepLinkUrlError = validateDeepLink(iosContent.deepLinkValue, formatMessage, messages);
1565
+
1566
+ setAndroidDeepLinkError(androidDeepLinkUrlError || "");
1567
+ setIosDeepLinkError(iosDeepLinkUrlError || "");
1568
+
1569
+ // Check for carousel link errors
1570
+ const hasCarouselErrors = Object.values(carouselLinkErrors).some((error) => error !== null && error !== "");
1571
+
1572
+ if (androidUrlError || iosUrlError || androidDeepLinkUrlError || iosDeepLinkUrlError || hasCarouselErrors) {
1573
+ CapNotification.error({
1574
+ message: formatMessage(messages.invalidUrl),
1575
+ });
1576
+ if (onValidationFail) {
1577
+ onValidationFail();
1578
+ }
1579
+ return;
1580
+ }
1581
+
1582
+ try {
1583
+ // Convert ctaData to backend format and add to content
1584
+ const processedAndroidContent = { ...androidContent };
1585
+ const processedIosContent = { ...iosContent };
1586
+
1587
+ // Process button CTA data if it exists
1588
+ if (ctaDataAndroid && ctaDataAndroid.length > 0) {
1589
+ const savedCtas = ctaDataAndroid.filter((cta) => cta.isSaved);
1590
+ if (savedCtas.length > 0) {
1591
+ processedAndroidContent.expandableDetails = {
1592
+ ...processedAndroidContent.expandableDetails,
1593
+ ctas: savedCtas.map((cta) => ({
1594
+ actionText: cta.text,
1595
+ type: cta.urlType || DEEP_LINK,
1596
+ actionLink: cta.url,
1597
+ })),
1598
+ };
1599
+ }
1600
+ }
1601
+ if (ctaDataIos && ctaDataIos.length > 0) {
1602
+ const savedCtas = ctaDataIos.filter((cta) => cta.isSaved);
1603
+ if (savedCtas.length > 0) {
1604
+ processedIosContent.expandableDetails = {
1605
+ ...processedIosContent.expandableDetails,
1606
+ ctas: savedCtas.map((cta) => ({
1607
+ actionText: cta.text,
1608
+ type: cta.urlType || DEEP_LINK,
1609
+ actionLink: cta.url,
1610
+ })),
1611
+ };
1612
+ }
1613
+ }
1614
+
1615
+ // Fix: Only include enabled platform content in payload
1616
+ const payload = createPayload({
1617
+ templateName: finalTemplateName,
1618
+ androidContent: isAndroidSupported ? processedAndroidContent : undefined,
1619
+ iosContent: isIosSupported ? processedIosContent : undefined,
1620
+ imageSrc,
1621
+ mpushVideoSrcAndPreview,
1622
+ accountData,
1623
+ sameContent,
1624
+ options: {
1625
+ mode: params?.mode,
1626
+ },
1627
+ });
1628
+
1629
+ // Add template ID for edit mode
1630
+ if (isEditMode && templateId) {
1631
+ payload._id = templateId;
1632
+ }
1633
+
1634
+ const content = getContent(payload);
1635
+ const { definition: { mode: definitionMode } = {} } = payload || {};
1636
+ const label = definitionMode === IMAGE.toLowerCase() ? IMAGE.toLowerCase() : TEXT.toLowerCase();
1637
+
1638
+ const handleSuccess = (response) => {
1639
+ const timeTaken = GA.timeTracker.stopTimer(TRACK_CREATE_MPUSH, {
1640
+ category: "Creatives",
1641
+ action: TRACK_CREATE_MPUSH,
1642
+ label,
1643
+ });
1644
+
1645
+ gtmPush("creativeDetails", {
1646
+ id: response?.templateId || params?.id,
1647
+ name: payload.name,
1648
+ channel: MOBILE_PUSH_CHANNEL,
1649
+ timeTaken,
1650
+ content,
1651
+ mode: isEditMode ? EDIT : CREATE,
1652
+ imageAdded: definitionMode === IMAGE.toLowerCase(),
1653
+ });
1654
+
1655
+ // --- BEGIN: Library mode communication fix ---
1656
+ if (!isFullMode) {
1657
+ // In library mode, only communicate to parent/callback, do NOT call create/edit API
1658
+ const formDataForLibrary = {
1659
+ action: 'getFormData',
1660
+ value: payload, // payload is the backend-ready template data with versions.base structure
1661
+ validity: true,
1662
+ type: 'MOBILEPUSH',
1663
+ };
1664
+ if (window && window.parent) {
1665
+ window.parent.postMessage(JSON.stringify(formDataForLibrary), '*');
1666
+ }
1667
+ if (typeof getFormLibraryData === 'function') {
1668
+ try {
1669
+ getFormLibraryData(formDataForLibrary);
1670
+ } catch (error) {
1671
+ console.error('[MobilePushNew] Error calling getFormLibraryData:', error);
1672
+ }
1673
+ }
1674
+
1675
+ // Close slidebox after successful save in library mode
1676
+ if (typeof handleClose === 'function') {
1677
+ handleClose();
1678
+ }
1679
+
1680
+ // Do not proceed with any API or further side effects in library mode
1681
+ return;
1682
+ }
1683
+ // --- END: Library mode communication fix ---
1684
+
1685
+ // Show success toast and refresh list after create (not edit)
1686
+ if (!isEditMode && response && (response.templateId || response._id)) {
1687
+ const message = `${intl.formatMessage(messages.templateCreateSuccess)}`;
1688
+ CapNotification.success({ key: 'createSuccess', message });
1689
+ // Call handleClose to close the slidebox/modal
1690
+ if (typeof handleClose === 'function') {
1691
+ handleClose();
1692
+ }
1693
+ // Call onCreateComplete to notify parent to refresh list
1694
+ if (typeof onCreateComplete === 'function') {
1695
+ onCreateComplete(true);
1696
+ }
1697
+ // Optionally, trigger a parent refresh (if needed)
1698
+ if (window && window.parent) {
1699
+ window.parent.postMessage(JSON.stringify({ type: 'REFRESH_MOBILEPUSH_TEMPLATES' }), '*');
1700
+ }
1701
+ setTimeout(() => {
1702
+ mobilePushActions.clearCreateResponse();
1703
+ }, 100);
1704
+ }
1705
+
1706
+ if (isEditMode) {
1707
+ // Delay handleClose to ensure Templates container processes the response first
1708
+ setTimeout(() => {
1709
+ try {
1710
+ handleClose();
1711
+ } catch (error) {
1712
+ console.error("[MobilePushNew] Error calling handleClose:", error);
1713
+ }
1714
+ }, 150);
1715
+ // Delay clearing the response to allow Templates container to process it
1716
+ setTimeout(() => {
1717
+ mobilePushActions.clearEditResponse();
1718
+ }, 100);
1719
+ } else {
1720
+ resetFormData();
1721
+ }
1722
+
1723
+ if (getFormLibraryData && isFullMode) {
1724
+ getFormLibraryData({ validity: true });
1725
+ }
1726
+ };
1727
+
1728
+ // --- Only call create/edit API in full mode ---
1729
+ if (isFullMode) {
1730
+ if (isEditMode) {
1731
+ mobilePushActions.editTemplate(payload, (response) => {
1732
+ // Guard: If response is an Error object, show error notification and return
1733
+ if (response instanceof Error) {
1734
+ CapNotification.error({ key: 'createError', message: intl.formatMessage(messages.somethingWentWrong) });
1735
+ return;
1736
+ }
1737
+ handleSuccess({ ...response, isEdit: true });
1738
+ });
1739
+ } else {
1740
+ mobilePushActions.createTemplate(payload, (response) => {
1741
+ // Guard: If response is an Error object, show error notification and return
1742
+ if (response instanceof Error) {
1743
+ const errorMsg = response.message || response.toString();
1744
+ // Check for duplicate name error
1745
+ if (errorMsg && errorMsg.toLowerCase().includes('template name already exist')) {
1746
+ CapNotification.error({ key: 'duplicateName', message: errorMsg });
1747
+ // Do NOT close the slidebox or call onCreateComplete
1748
+ return;
1749
+ }
1750
+ CapNotification.error({ key: 'createError', message: intl.formatMessage(messages.somethingWentWrong) });
1751
+ if (typeof onCreateComplete === 'function') {
1752
+ onCreateComplete(false);
1753
+ }
1754
+ return;
1755
+ }
1756
+ handleSuccess({ ...response, isEdit: false });
1757
+ });
1758
+ }
1759
+ } else {
1760
+ // In library mode, do NOT call create/edit API, just communicate to parent/callback (already handled above)
1761
+ // Call handleSuccess directly to trigger the postMessage logic
1762
+ handleSuccess({ isLibraryMode: true });
1763
+ }
1764
+ } catch (error) {
1765
+ const errorMsg = getMessageObject(
1766
+ "error",
1767
+ formatMessage(messages.somethingWentWrong),
1768
+ true
1769
+ );
1770
+ globalActionsProps.addMessageToQueue(errorMsg);
1771
+ if (onValidationFail) {
1772
+ onValidationFail();
1773
+ }
1774
+ }
1775
+ }, [
1776
+ templateNameError,
1777
+ formatMessage,
1778
+ templateName,
1779
+ androidContent,
1780
+ iosContent,
1781
+ imageSrc,
1782
+ mpushVideoSrcAndPreview,
1783
+ accountData,
1784
+ params?.mode,
1785
+ params?.id,
1786
+ isEditMode,
1787
+ mobilePushActions,
1788
+ handleClose,
1789
+ getFormLibraryData,
1790
+ globalActionsProps,
1791
+ onValidationFail,
1792
+ resetFormData,
1793
+ validateExternalLink,
1794
+ ctaDataAndroid,
1795
+ ctaDataIos,
1796
+ intl,
1797
+ onCreateComplete,
1798
+ createTimeoutRef,
1799
+ isAndroidSupported,
1800
+ isIosSupported,
1801
+ isAndroidFieldsMissing,
1802
+ isIosFieldsMissing,
1803
+ isCarouselDataValid,
1804
+ isFullMode,
1805
+ templateId,
1806
+ ]);
1807
+
1808
+ // Helper to sync content between platforms
1809
+ const syncPlatformContent = useCallback((sourceContent, sourcePlatform) => {
1810
+ const targetPlatform = sourcePlatform === ANDROID ? IOS : ANDROID;
1811
+ const setTargetContent = sourcePlatform === ANDROID ? setIosContent : setAndroidContent;
1812
+ const setTargetTitleError = sourcePlatform === ANDROID ? setIosTitleError : setAndroidTitleError;
1813
+ const setTargetMessageError = sourcePlatform === ANDROID ? setIosMessageError : setAndroidMessageError;
1814
+ // Sync all content fields
1815
+ setTargetContent({
1816
+ ...sourceContent,
1817
+ // Preserve platform-specific fields if any
1818
+ buttons: Array.isArray(sourceContent.buttons)
1819
+ ? sourceContent.buttons.map((btn) => ({
1820
+ ...btn,
1821
+ platform: targetPlatform,
1822
+ }))
1823
+ : [],
1824
+ });
1825
+
1826
+ // Validate synced content
1827
+ setTargetTitleError(validateTitle(sourceContent.title));
1828
+ setTargetMessageError(validateMessage(sourceContent.message));
1829
+ }, []);
1830
+
1831
+ const onTemplateNameChange = useCallback(
1832
+ (ev) => {
1833
+ const { value } = ev.target;
1834
+ setTemplateName(value);
1835
+ const isInvalid = !value || value.trim() === "";
1836
+ setTemplateNameError(isInvalid);
1837
+ if (value && onEnterTemplateName) {
1838
+ onEnterTemplateName();
1839
+ } else if (onRemoveTemplateName) {
1840
+ onRemoveTemplateName();
1841
+ }
1842
+ },
1843
+ [onEnterTemplateName, onRemoveTemplateName]
1844
+ );
1845
+
1846
+ // --- Only show template name input in full mode (not library/consumer mode) ---
1847
+ const createModeContent = useCallback(
1848
+ () => (
1849
+ isFullMode ? (
1850
+ <CapRow className="input-group creative-name-container">
1851
+ <CapInput
1852
+ id="mobile-push-template-name-input"
1853
+ className="mobile-push-template-name-input"
1854
+ key={`template-name-${params?.id || 'create'}`}
1855
+ label={formatMessage(messages.creativeName)}
1856
+ placeholder={formatMessage(messages.creativeNamePlaceholder)}
1857
+ value={templateName}
1858
+ onChange={onTemplateNameChange}
1859
+ status={templateNameError ? "error" : ""}
1860
+ help={
1861
+ templateNameError
1862
+ ? formatMessage(messages.emptyTemplateErrorMessage)
1863
+ : ""
1864
+ }
1865
+ />
1866
+ </CapRow>
1867
+ ) : null
1868
+ ),
1869
+ [isFullMode, formatMessage, templateName, onTemplateNameChange, templateNameError, params?.id]
1870
+ );
1871
+
1872
+ const liquidMiddleWare = useCallback(() => {
1873
+ const onError = ({ standardErrors, liquidErrors }) => {
1874
+ setErrorMessage((prev) => ({
1875
+ STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
1876
+ LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
1877
+ }));
1878
+ };
1879
+ const onSuccess = () => handleSave();
1880
+
1881
+ validateMobilePushContent([androidContent, iosContent], {
1882
+ currentTab: activeTab === ANDROID ? 1 : 2,
1883
+ onError,
1884
+ onSuccess,
1885
+ getLiquidTags: globalActionsProps.getLiquidTags,
1886
+ formatMessage,
1887
+ messages: formBuilderMessages,
1888
+ tagLookupMap: metaEntities?.tagLookupMap || {},
1889
+ eventContextTags: metaEntities?.eventContextTags || [],
1890
+ isLiquidFlow: hasLiquidSupportFeature(),
1891
+ forwardedTags: {},
1892
+ skipTags: (tag) => {
1893
+ const skipRegexes = [
1894
+ /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
1895
+ /unsubscribe\(#[a-zA-Z\d]{6}\)/,
1896
+ /Link_to_[a-zA-z]/,
1897
+ /SURVEY.*\.TOKEN/,
1898
+ /^[A-Za-z].*\([a-zA-Z\d]*\)/,
1899
+ ];
1900
+ return skipRegexes.some((regex) => regex.test(tag));
1901
+ },
1902
+ singleTab: getSingleTab(accountData),
1903
+ });
1904
+ }, [
1905
+ androidContent,
1906
+ iosContent,
1907
+ activeTab,
1908
+ globalActionsProps,
1909
+ formatMessage,
1910
+ metaEntities,
1911
+ accountData,
1912
+ ]);
1913
+
1914
+ const isLiquidFlow = hasLiquidSupportFeature();
1915
+
1916
+ useEffect(() => {
1917
+ // Always map to { label } for both platforms
1918
+ const newButtons = Array.isArray(ctaData)
1919
+ ? ctaData.map((data) => ({
1920
+ label: data?.text || '',
1921
+ }))
1922
+ : [];
1923
+
1924
+ if (activeTab === ANDROID) {
1925
+ setAndroidContent((prevContent) => ({
1926
+ ...prevContent,
1927
+ buttons: newButtons,
1928
+ }));
1929
+ } else if (activeTab === IOS) {
1930
+ setIosContent((prevContent) => ({
1931
+ ...prevContent,
1932
+ buttons: newButtons,
1933
+ }));
1934
+ }
1935
+ }, [ctaData, activeTab]);
1936
+
1937
+ // Sync button data and state for both platforms when sameContent is enabled (e.g., in edit mode)
1938
+ useEffect(() => {
1939
+ if (sameContent) {
1940
+ // Copy button data and state from the active tab to the other platform
1941
+ if (activeTab === ANDROID) {
1942
+ setCtaDataIos(ctaDataAndroid);
1943
+ setPrimaryButtonIos(primaryButtonAndroid);
1944
+ setSecondaryButtonIos(secondaryButtonAndroid);
1945
+ } else {
1946
+ setCtaDataAndroid(ctaDataIos);
1947
+ setPrimaryButtonAndroid(primaryButtonIos);
1948
+ setSecondaryButtonAndroid(secondaryButtonIos);
1949
+ }
1950
+ }
1951
+ // Only run when sameContent or activeTab changes
1952
+ }, [sameContent]);
1953
+
1954
+ const handleSameContentChange = useCallback((e) => {
1955
+ const isCurrentPlatformAndroid = activeTab === ANDROID;
1956
+ syncPlatformContent(isCurrentPlatformAndroid ? androidContent : iosContent, isCurrentPlatformAndroid ? ANDROID : IOS);
1957
+ setSameContent(e.target.checked);
1958
+ }, [activeTab, androidContent, iosContent]);
1959
+
1960
+ const getContentType = useCallback(
1961
+ (content) => {
1962
+ // Get image source from multiple possible locations
1963
+ const getImageSource = () => {
1964
+ // First check content-specific image - handle both string and object formats
1965
+ if (content?.imageSrc) {
1966
+ // If imageSrc is a string, return it directly
1967
+ if (typeof content.imageSrc === 'string') {
1968
+ return content.imageSrc;
1969
+ }
1970
+ // If imageSrc is an object, extract the URL based on active tab
1971
+ if (typeof content.imageSrc === 'object') {
1972
+ if (activeTab === ANDROID && content.imageSrc.androidImageSrc) {
1973
+ return content.imageSrc.androidImageSrc;
1974
+ }
1975
+ if (activeTab === IOS && content.imageSrc.iosImageSrc) {
1976
+ return content.imageSrc.iosImageSrc;
1977
+ }
1978
+ return '';
1979
+ }
1980
+ }
1981
+
1982
+ // Then check upload state based on active tab
1983
+ if (activeTab === ANDROID && imageSrc?.androidImageSrc) {
1984
+ return imageSrc.androidImageSrc;
1985
+ }
1986
+ if (activeTab === IOS && imageSrc?.iosImageSrc) {
1987
+ return imageSrc.iosImageSrc;
1988
+ }
1989
+
1990
+ return '';
1991
+ };
1992
+
1993
+ // Get video source from multiple possible locations
1994
+ const getVideoSource = () => {
1995
+ // First check current platform's content
1996
+ if (content?.videoSrc) {
1997
+ return content.videoSrc;
1998
+ }
1999
+
2000
+ // Then check current platform's upload state only - DEFENSIVE: ensure perfect symmetry
2001
+ const uploadVideoSrc = activeTab === ANDROID
2002
+ ? videoState?.androidVideoSrc || ''
2003
+ : videoState?.iosVideoSrc || '';
2004
+ return uploadVideoSrc;
2005
+ };
2006
+
2007
+ const getVideoPreview = () => {
2008
+ // First check current platform's content
2009
+ if (content?.videoPreview) {
2010
+ return content.videoPreview;
2011
+ }
2012
+
2013
+ // Then check current platform's upload state only - DEFENSIVE: ensure perfect symmetry
2014
+ const uploadVideoPreview = activeTab === ANDROID
2015
+ ? videoState?.androidVideoPreview || ''
2016
+ : videoState?.iosVideoPreview || '';
2017
+ return uploadVideoPreview;
2018
+ };
2019
+
2020
+ const videoSrc = getVideoSource();
2021
+ const videoPreview = getVideoPreview();
2022
+
2023
+ const bodyGifValue = content?.mediaType === GIF ? (videoSrc || getImageSource()) : "";
2024
+
2025
+ const previewData = {
2026
+ appName: editData?.selectedWeChatAccount?.name || accountData?.selectedWeChatAccount?.name || "",
2027
+ bodyText: content?.message || "",
2028
+ bodyImage: content?.mediaType === IMAGE ? getImageSource() : "",
2029
+ actions: Array.isArray(content?.buttons) ? content.buttons : [],
2030
+ carouselData: content?.carouselData || [],
2031
+ header: content?.title || "",
2032
+ bodyVideo: (content?.mediaType === VIDEO || content?.mediaType === GIF) ? {
2033
+ videoSrc,
2034
+ videoPreview,
2035
+ } : {},
2036
+ bodyGif: bodyGifValue,
2037
+ };
2038
+ return previewData;
2039
+ },
2040
+ [editData, accountData, imageSrc, mpushVideoSrcAndPreview, activeTab, videoState]
2041
+ );
2042
+
2043
+ const previewContent = useMemo(() => {
2044
+ const currentContent = activeTab === ANDROID ? androidContent : iosContent;
2045
+ const preview = getContentType(currentContent);
2046
+ return preview;
2047
+ }, [activeTab, androidContent, iosContent, getContentType]);
2048
+
2049
+ // Add useEffect to handle isGetFormData prop changes
2050
+ useEffect(() => {
2051
+ if (isGetFormData) {
2052
+ handleSave();
2053
+ // Reset the flag to prevent infinite loop
2054
+ if (onValidationFail) {
2055
+ onValidationFail();
2056
+ }
2057
+ }
2058
+ }, [isGetFormData, handleSave, onValidationFail]);
2059
+
2060
+ // Add message event listener to handle parent communication (like old MobilePush components)
2061
+ useEffect(() => {
2062
+ const handleFrameTasks = (e) => {
2063
+ const { data: type } = e;
2064
+
2065
+ if (typeof type === 'object') {
2066
+ const { action } = type;
2067
+ switch (action) {
2068
+ case "getFormData":
2069
+ handleSave();
2070
+ break;
2071
+ case "startTemplateCreation":
2072
+ // Handle template creation start if needed
2073
+ break;
2074
+ default:
2075
+ break;
2076
+ }
2077
+ } else {
2078
+ switch (type) {
2079
+ case "getFormData":
2080
+ handleSave();
2081
+ break;
2082
+ default:
2083
+ break;
2084
+ }
2085
+ }
2086
+ };
2087
+
2088
+ window.addEventListener("message", handleFrameTasks);
2089
+
2090
+ return () => {
2091
+ window.removeEventListener("message", handleFrameTasks);
2092
+ };
2093
+ }, [handleSave]);
2094
+
2095
+ return (
2096
+ <CapSpin
2097
+ spinning={spin || editData?.editTemplateInProgress || fetchingLiquidValidation || getTemplateDetailsInProgress}
2098
+ tip={
2099
+ fetchingLiquidValidation && formatMessage(messages.validationLoadingMessage)
2100
+ }
2101
+ >
2102
+ <CapRow className="mobile-push-container">
2103
+ <CapColumn className="content-section" span={14}>
2104
+ {createModeContent()}
2105
+ {isAndroidSupported && isIosSupported && (
2106
+ <CapCheckbox
2107
+ className="same-content-checkbox"
2108
+ checked={sameContent}
2109
+ onChange={handleSameContentChange}
2110
+ key={`same-content-${sameContent}`}
2111
+ >
2112
+ {formatMessage(messages.sameContentForBothChannels)}
2113
+ </CapCheckbox>
2114
+ )}
2115
+ <CapRow className="platform-header">
2116
+ <CapTab
2117
+ className="platform-tabs"
2118
+ activeKey={activeTab}
2119
+ onChange={setActiveTab}
2120
+ panes={PANES}
2121
+ />
2122
+ </CapRow>
2123
+ <CapRow>
2124
+ <CapColumn span={24}>
2125
+ <ErrorInfoNote
2126
+ currentTab={activeTab?.toUpperCase()}
2127
+ errorMessages={{
2128
+ LIQUID_ERROR_MSG: errorMessage?.LIQUID_ERROR_MSG,
2129
+ STANDARD_ERROR_MSG: errorMessage?.STANDARD_ERROR_MSG,
2130
+ }}
2131
+ />
2132
+
2133
+ {/* In library mode, only show save button in create mode (edit mode has Done button from FormBuilder). In full mode, keep old logic. */}
2134
+ {((!isFullMode && computedCreativesMode !== 'edit') || (isFullMode && !isEditMode && computedCreativesMode === 'create')) && (
2135
+ <CapRow className="save-section">
2136
+ <CapColumn span={24}>
2137
+ <CapButton
2138
+ type="primary"
2139
+ onClick={() => {
2140
+ if (isLiquidFlow) {
2141
+ liquidMiddleWare();
2142
+ } else {
2143
+ handleSave();
2144
+ }
2145
+ }}
2146
+ className="save-button"
2147
+ disabled={isSaveDisabled}
2148
+ >
2149
+ {formatMessage(messages.saveTemplate)}
2150
+ </CapButton>
2151
+ </CapColumn>
2152
+ </CapRow>
2153
+ )}
2154
+
2155
+ </CapColumn>
2156
+ </CapRow>
2157
+ </CapColumn>
2158
+ <CapColumn className="preview-section" span={10}>
2159
+ <TemplatePreview
2160
+ device={activeTab === ANDROID ? "android" : "iphone"}
2161
+ content={previewContent}
2162
+ channel={MOBILE_PUSH_CHANNEL}
2163
+ templateData={templateData}
2164
+ />
2165
+ </CapColumn>
2166
+ </CapRow>
2167
+ </CapSpin>
2168
+ );
2169
+ };
2170
+
2171
+ MobilePushNew.propTypes = {
2172
+ isFullMode: PropTypes.bool,
2173
+ onEnterTemplateName: PropTypes.func,
2174
+ onRemoveTemplateName: PropTypes.func,
2175
+ intl: intlShape.isRequired,
2176
+ location: PropTypes.object,
2177
+ selectedOfferDetails: PropTypes.object,
2178
+ getDefaultTags: PropTypes.func,
2179
+ injectedTags: PropTypes.object,
2180
+ params: PropTypes.object,
2181
+ templateData: PropTypes.object,
2182
+ accountData: PropTypes.object,
2183
+ editData: PropTypes.object,
2184
+ mobilePushActions: PropTypes.object,
2185
+ onValidationFail: PropTypes.func,
2186
+ getFormLibraryData: PropTypes.func,
2187
+ uploadedAssetData: PropTypes.object,
2188
+ uploadedAssetData0: PropTypes.object,
2189
+ uploadedAssetData1: PropTypes.object,
2190
+ uploadAssetSuccess: PropTypes.bool,
2191
+ metaEntities: PropTypes.object,
2192
+ supportedTags: PropTypes.array,
2193
+ globalActions: PropTypes.object,
2194
+ fetchingLiquidValidation: PropTypes.bool,
2195
+ handleClose: PropTypes.func,
2196
+ createTemplateError: PropTypes.any,
2197
+ isGetFormData: PropTypes.bool,
2198
+ getTemplateDetailsInProgress: PropTypes.bool,
2199
+ onCreateComplete: PropTypes.func,
2200
+ };
2201
+
2202
+ MobilePushNew.defaultProps = {
2203
+ isFullMode: false,
2204
+ onEnterTemplateName: () => {},
2205
+ onRemoveTemplateName: () => {},
2206
+ location: {},
2207
+ selectedOfferDetails: {},
2208
+ getDefaultTags: () => {},
2209
+ injectedTags: {},
2210
+ params: {},
2211
+ templateData: {},
2212
+ accountData: {},
2213
+ editData: {},
2214
+ mobilePushActions: {},
2215
+ onValidationFail: () => {},
2216
+ getFormLibraryData: () => {},
2217
+ uploadedAssetData: {},
2218
+ uploadedAssetData0: {},
2219
+ uploadedAssetData1: {},
2220
+ uploadAssetSuccess: false,
2221
+ metaEntities: {},
2222
+ supportedTags: [],
2223
+ globalActions: {},
2224
+ fetchingLiquidValidation: false,
2225
+ handleClose: () => {},
2226
+ createTemplateError: null,
2227
+ isGetFormData: false,
2228
+ getTemplateDetailsInProgress: false,
2229
+ onCreateComplete: () => {},
2230
+ };
2231
+
2232
+ const mapStateToProps = createStructuredSelector({
2233
+ editData: makeSelectMobilePushNew(),
2234
+ injectedTags: setInjectedTags(),
2235
+ currentOrgDetails: selectCurrentOrgDetails(),
2236
+ metaEntities: makeSelectMetaEntities(),
2237
+ loadingTags: isLoadingMetaEntities(),
2238
+ uploadedAssetData: makeSelectUploadedAssetData(),
2239
+ uploadedAssetData0: makeSelectUploadedAssetData0(),
2240
+ uploadedAssetData1: makeSelectUploadedAssetData1(),
2241
+ uploadAssetSuccess: makeSelectUploadAssetSuccess(),
2242
+ assetUploading: makeSelectAssetUploading(),
2243
+ fetchingLiquidValidation: selectLiquidStateDetails(),
2244
+ createTemplateError: makeSelectCreateError(),
2245
+ supportedTags: () => [],
2246
+ accountData: createSelector(
2247
+ (state) => state.get('templates'),
2248
+ (templatesState) => {
2249
+ if (!templatesState) return {};
2250
+ const templates = templatesState.toJS();
2251
+ return templates?.selectedWeChatAccount || {};
2252
+ }
2253
+ ),
2254
+ getTemplateDetailsInProgress: makeSelectGetTemplateDetailsInProgress(),
2255
+ });
2256
+
2257
+ const mapDispatchToProps = (dispatch) => ({
2258
+ mobilePushActions: bindActionCreators(actions, dispatch),
2259
+ globalActions: bindActionCreators(globalActions, dispatch),
2260
+ });
2261
+
2262
+ const withSaga = injectSaga({
2263
+ key: "mobilePushNew",
2264
+ saga: v2MobilePushSagas,
2265
+ mode: DAEMON,
2266
+ });
2267
+
2268
+ const withReducer = injectReducer({
2269
+ key: "mobilePushNew",
2270
+ reducer: mobilePushReducer,
2271
+ });
2272
+
2273
+ export default withCreatives({
2274
+ WrappedComponent: MobilePushNew,
2275
+ mapStateToProps,
2276
+ mapDispatchToProps,
2277
+ userAuth: true,
2278
+ sagas: [withSaga],
2279
+ reducers: [withReducer],
2280
+ });