@capillarytech/creatives-library 8.0.130 → 8.0.131

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