@capillarytech/creatives-library 8.0.129 → 8.0.130

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 (76) hide show
  1. package/containers/App/constants.js +0 -1
  2. package/containers/Templates/constants.js +10 -1
  3. package/containers/Templates/index.js +45 -45
  4. package/package.json +1 -1
  5. package/services/api.js +9 -7
  6. package/services/tests/haptic-api.test.js +387 -0
  7. package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +3 -8
  8. package/tests/integration/TemplateCreation/api-response.js +0 -5
  9. package/tests/integration/TemplateCreation/msw-handler.js +63 -42
  10. package/utils/common.js +0 -7
  11. package/utils/commonUtils.js +6 -2
  12. package/utils/tests/vendorDataTransformers.test.js +512 -0
  13. package/utils/vendorDataTransformers.js +108 -0
  14. package/v2Components/CapDocumentUpload/index.js +2 -2
  15. package/v2Components/CapImageUpload/index.js +46 -59
  16. package/v2Components/CapInAppCTA/index.js +0 -1
  17. package/v2Components/CapTagList/index.js +120 -177
  18. package/v2Components/CapVideoUpload/constants.js +0 -3
  19. package/v2Components/CapVideoUpload/index.js +110 -167
  20. package/v2Components/CapVideoUpload/messages.js +0 -16
  21. package/v2Components/Carousel/index.js +13 -15
  22. package/v2Components/ErrorInfoNote/style.scss +0 -1
  23. package/v2Components/MobilePushPreviewV2/index.js +5 -37
  24. package/v2Components/TemplatePreview/_templatePreview.scss +72 -114
  25. package/v2Components/TemplatePreview/index.js +50 -178
  26. package/v2Components/TemplatePreview/messages.js +0 -4
  27. package/v2Containers/CreativesContainer/SlideBoxContent.js +62 -127
  28. package/v2Containers/CreativesContainer/index.js +136 -191
  29. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +22 -0
  30. package/v2Containers/InApp/constants.js +0 -1
  31. package/v2Containers/InApp/index.js +13 -13
  32. package/v2Containers/MobilePush/Create/index.js +0 -1
  33. package/v2Containers/MobilePush/commonMethods.js +14 -7
  34. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +5 -5
  35. package/v2Containers/TagList/index.js +10 -56
  36. package/v2Containers/Templates/_templates.scss +1 -101
  37. package/v2Containers/Templates/index.js +41 -151
  38. package/v2Containers/Templates/messages.js +0 -8
  39. package/v2Containers/Templates/sagas.js +0 -2
  40. package/v2Containers/Whatsapp/constants.js +32 -1
  41. package/v2Containers/Whatsapp/index.js +104 -25
  42. package/v2Containers/Whatsapp/tests/haptic.test.js +405 -0
  43. package/utils/createPayload.js +0 -405
  44. package/utils/tests/createPayload.test.js +0 -785
  45. package/v2Components/CapMpushCTA/constants.js +0 -25
  46. package/v2Components/CapMpushCTA/index.js +0 -402
  47. package/v2Components/CapMpushCTA/index.scss +0 -95
  48. package/v2Components/CapMpushCTA/messages.js +0 -101
  49. package/v2Components/TemplatePreview/assets/images/Android _ With date and time.svg +0 -29
  50. package/v2Components/TemplatePreview/assets/images/android.svg +0 -9
  51. package/v2Components/TemplatePreview/assets/images/iOS _ With date and time.svg +0 -26
  52. package/v2Components/TemplatePreview/assets/images/ios.svg +0 -9
  53. package/v2Containers/MobilePushNew/actions.js +0 -116
  54. package/v2Containers/MobilePushNew/components/CtaButtons.js +0 -181
  55. package/v2Containers/MobilePushNew/components/MediaUploaders.js +0 -834
  56. package/v2Containers/MobilePushNew/components/PlatformContentFields.js +0 -345
  57. package/v2Containers/MobilePushNew/components/index.js +0 -5
  58. package/v2Containers/MobilePushNew/components/tests/CtaButtons.test.js +0 -798
  59. package/v2Containers/MobilePushNew/components/tests/MediaUploaders.test.js +0 -2114
  60. package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +0 -343
  61. package/v2Containers/MobilePushNew/constants.js +0 -115
  62. package/v2Containers/MobilePushNew/hooks/tests/usePlatformSync.test.js +0 -1299
  63. package/v2Containers/MobilePushNew/hooks/tests/useUpload.test.js +0 -1223
  64. package/v2Containers/MobilePushNew/hooks/usePlatformSync.js +0 -246
  65. package/v2Containers/MobilePushNew/hooks/useUpload.js +0 -726
  66. package/v2Containers/MobilePushNew/index.js +0 -3412
  67. package/v2Containers/MobilePushNew/index.scss +0 -308
  68. package/v2Containers/MobilePushNew/messages.js +0 -242
  69. package/v2Containers/MobilePushNew/reducer.js +0 -160
  70. package/v2Containers/MobilePushNew/sagas.js +0 -198
  71. package/v2Containers/MobilePushNew/selectors.js +0 -55
  72. package/v2Containers/MobilePushNew/tests/reducer.test.js +0 -741
  73. package/v2Containers/MobilePushNew/tests/sagas.test.js +0 -863
  74. package/v2Containers/MobilePushNew/tests/selectors.test.js +0 -425
  75. package/v2Containers/MobilePushNew/tests/utils.test.js +0 -322
  76. package/v2Containers/MobilePushNew/utils.js +0 -33
@@ -1,3412 +0,0 @@
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 [androidDeepLinkKeysError, setAndroidDeepLinkKeysError] = useState("");
252
- const [iosDeepLinkKeysError, setIosDeepLinkKeysError] = useState("");
253
- const [carouselLinkErrors, setCarouselLinkErrors] = useState({});
254
-
255
- // Ref for create timeout fallback
256
- const createTimeoutRef = useRef(null);
257
-
258
- // Function to validate carousel data completeness
259
- const validateCarouselData = useCallback((carouselData = []) => {
260
- if (!carouselData || carouselData.length === 0) {
261
- return false;
262
- }
263
-
264
- return carouselData.every((card) => {
265
- // Check if image is uploaded (currently only image media type is supported)
266
- if (card.mediaType === 'image' && !card.imageUrl) {
267
- return false;
268
- }
269
-
270
- // Check if buttons have proper link data when actionOnClick is true
271
- if (card.buttons && card.buttons.length > 0) {
272
- return card.buttons.every((button) => {
273
- if (button.actionOnClick) {
274
- // Check if link is provided based on link type
275
- if (button.linkType === DEEP_LINK && !button.deepLinkValue) {
276
- return false;
277
- }
278
- if (button.linkType === DEEP_LINK && button.deepLinkValue) {
279
- // Check if the selected deep link has keys, and if so, require them
280
- const selectedDeepLink = deepLink?.find((link) => link?.value === button.deepLinkValue);
281
- const deepLinkKeysFromSelection = selectedDeepLink?.keys;
282
- let deepLinkKeysFromSelectionArray = [];
283
- if (Array.isArray(deepLinkKeysFromSelection)) {
284
- deepLinkKeysFromSelectionArray = deepLinkKeysFromSelection;
285
- } else if (deepLinkKeysFromSelection) {
286
- deepLinkKeysFromSelectionArray = [deepLinkKeysFromSelection];
287
- }
288
-
289
- // Only require deep link keys if the selected deep link has keys defined
290
- if (deepLinkKeysFromSelectionArray.length > 0 && (!Array.isArray(button.deepLinkKeys) || button.deepLinkKeys.length === 0)) {
291
- return false;
292
- }
293
- }
294
- if (button.linkType === EXTERNAL_LINK && !button.externalLinkValue) {
295
- return false;
296
- }
297
- }
298
- return true;
299
- });
300
- }
301
-
302
- return true;
303
- });
304
- }, [deepLink]);
305
-
306
- // Function to check if carousel data is valid for both platforms
307
- const isCarouselDataValid = useCallback(() => {
308
- // Check Android carousel data
309
- const androidCarouselValid = androidContent?.mediaType === CAROUSEL
310
- ? validateCarouselData(androidContent.carouselData)
311
- : true;
312
-
313
- // Check iOS carousel data (only if not using same content)
314
- let iosCarouselValid = true;
315
- if (sameContent) {
316
- iosCarouselValid = androidCarouselValid; // If same content, iOS uses same data as Android
317
- } else if (iosContent?.mediaType === CAROUSEL) {
318
- iosCarouselValid = validateCarouselData(iosContent.carouselData);
319
- }
320
-
321
- return androidCarouselValid && iosCarouselValid;
322
- }, [androidContent?.mediaType, androidContent?.carouselData, iosContent?.mediaType, iosContent?.carouselData, sameContent, validateCarouselData]);
323
-
324
- // Define resetFormData early to avoid initialization errors
325
- const resetFormData = useCallback(() => {
326
- setTemplateName("");
327
- setAndroidContent({
328
- title: "",
329
- message: "",
330
- mediaType: NONE,
331
- buttons: [],
332
- actionOnClick: false,
333
- linkType: DEEP_LINK,
334
- deepLinkValue: "",
335
- deepLinkKeysValue: [],
336
- externalLinkValue: "",
337
- carouselData: [],
338
- });
339
- setIosContent({
340
- title: "",
341
- message: "",
342
- mediaType: NONE,
343
- buttons: [],
344
- actionOnClick: false,
345
- linkType: DEEP_LINK,
346
- deepLinkValue: "",
347
- deepLinkKeysValue: [],
348
- externalLinkValue: "",
349
- carouselData: [],
350
- });
351
- // Update resetFormData to reset both ctaDataAndroid and ctaDataIos
352
- setCtaDataAndroid([]);
353
- setCtaDataIos([]);
354
- setPrimaryButtonAndroid(false);
355
- setSecondaryButtonAndroid(false);
356
- setPrimaryButtonIos(false);
357
- setSecondaryButtonIos(false);
358
- setSameContent(false);
359
- setTemplateNameError(false);
360
- setAndroidTitleError("");
361
- setAndroidMessageError("");
362
- setIosTitleError("");
363
- setIosMessageError("");
364
- setAndroidExternalLinkError("");
365
- setIosExternalLinkError("");
366
- setAndroidDeepLinkError("");
367
- setIosDeepLinkError("");
368
- setCarouselLinkErrors({});
369
- resetUploadStates();
370
- }, [resetUploadStates]);
371
-
372
- useEffect(() => {
373
- if (createTemplateError) {
374
- CapNotification.error({
375
- message: createTemplateError,
376
- key: "createMpushError",
377
- });
378
- return;
379
- }
380
-
381
- // Prevent duplicate schema fetching
382
- if (schemaFetched.current) return;
383
-
384
- const { type, module } = location?.query || {};
385
- const isEmbedded = type === EMBEDDED;
386
- const context = isEmbedded ? module : DEFAULT;
387
- const embedded = isEmbedded ? type : FULL;
388
- const query = {
389
- layout: 'mobilepush',
390
- type: TAG,
391
- context,
392
- embedded,
393
- };
394
- if (getDefaultTags) {
395
- query.context = getDefaultTags;
396
- }
397
- globalActionsProps.fetchSchemaForEntity(query);
398
- schemaFetched.current = true;
399
- }, [createTemplateError, location?.query?.type, location?.query?.module, getDefaultTags]);
400
-
401
- useEffect(() => {
402
- let tag = get(metaEntities, `tags.standard`, []);
403
- const { type, module } = location?.query || {};
404
- if (type === EMBEDDED && module === LIBRARY && !getDefaultTags) {
405
- tag = supportedTags;
406
- }
407
- updateTags(tag);
408
- }, [metaEntities, location, getDefaultTags, supportedTags]);
409
-
410
- const templateDescErrorHandler = useCallback(
411
- (value) => {
412
- let errorTemplateDescMessage = "";
413
-
414
- const { unsupportedTags, isBraceError } = validateTags({
415
- content: value,
416
- tagsParam: tags,
417
- injectedTagsParams: injectedTags,
418
- location,
419
- tagModule: getDefaultTags,
420
- }) || {};
421
- if (value === "" && MEDIA_TYPES_OPTIONS[0].value) {
422
- errorTemplateDescMessage = formatMessage(
423
- messages.emptyTemplateDescErrorMessage
424
- );
425
- }
426
- if (unsupportedTags?.length > 0) {
427
- errorTemplateDescMessage = formatMessage(
428
- globalMessages.unsupportedTagsValidationError,
429
- {
430
- unsupportedTags,
431
- }
432
- );
433
- }
434
- if (isBraceError) {
435
- errorTemplateDescMessage = formatMessage(
436
- globalMessages.unbalanacedCurlyBraces
437
- );
438
- }
439
- return errorTemplateDescMessage;
440
- },
441
- [tags, injectedTags, location, getDefaultTags, formatMessage]
442
- );
443
-
444
- const validateTitle = useCallback(
445
- (value) => {
446
- let error = templateDescErrorHandler(value);
447
- if (!value || value.trim() === "") {
448
- error = formatMessage(messages.emptyTemplateDescErrorMessage);
449
- }
450
- return error || "";
451
- },
452
- [templateDescErrorHandler, formatMessage]
453
- );
454
-
455
- const validateMessage = useCallback(
456
- (value) => {
457
- let error = templateDescErrorHandler(value);
458
- if (!value || value.trim() === "") {
459
- error = formatMessage(messages.emptyTemplateDescErrorMessage);
460
- }
461
- return error || "";
462
- },
463
- [templateDescErrorHandler, formatMessage]
464
- );
465
-
466
- const handleOnTagsContextChange = useCallback(
467
- (data) => {
468
- const { type } = location?.query || {};
469
- const tempData = (data || "").toLowerCase();
470
- const isEmbedded = type === EMBEDDED;
471
- const embedded = isEmbedded ? type : FULL;
472
- const context = tempData === ALL ? DEFAULT : tempData;
473
- const query = {
474
- layout: 'mobilepush',
475
- type: TAG,
476
- context,
477
- embedded,
478
- };
479
- globalActionsProps.fetchSchemaForEntity(query);
480
- },
481
- [globalActionsProps, location]
482
- );
483
-
484
- useEffect(() => {
485
- // accountData IS the selectedWeChatAccount object based on mapStateToProps
486
- const accountObj = accountData || {};
487
- const deepLinkObj = accountObj?.configs?.deeplink || "";
488
-
489
- // Debug logging removed - issue identified and fixed
490
-
491
- if (!isEmpty(accountObj)) {
492
- const { configs = {} } = accountObj;
493
- const isAndroidSupported = configs?.android === DEVICE_SUPPORTED;
494
-
495
- try {
496
- // Parse deep link data - handle both object and array formats
497
- const parsedDeepLinks = JSON.parse(deepLinkObj || "[]");
498
- // Debug logging removed - issue identified and fixed
499
-
500
- let keys = [];
501
-
502
- if (Array.isArray(parsedDeepLinks)) {
503
- // Handle array format
504
- keys = parsedDeepLinks.map((link) => ({
505
- label: link?.name,
506
- value: link?.link,
507
- title: link?.link,
508
- keys: link?.keys,
509
- }));
510
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
511
- // Handle object format - convert to array
512
- keys = Object.values(parsedDeepLinks).map((link) => ({
513
- label: link?.name,
514
- value: link?.link,
515
- title: link?.link,
516
- keys: link?.keys,
517
- }));
518
- }
519
-
520
- // Debug logging removed - issue identified and fixed
521
-
522
- setActiveTab(isAndroidSupported ? ANDROID : IOS);
523
- setDeepLink(keys);
524
- } catch (error) {
525
- console.error("[MobilePushNew] Error parsing deeplinks:", error, { deepLinkObj });
526
- setDeepLink([]);
527
- }
528
- } else {
529
- // Debug logging removed - issue identified and fixed
530
- setDeepLink([]);
531
- }
532
- }, [accountData?.configs?.deeplink, accountData?.configs?.android, accountData]);
533
-
534
- // Add debug logs for template switching and data population
535
- useEffect(() => {
536
- resetFormData();
537
- if (params?.id) {
538
- setSpin(true);
539
- mobilePushActions.getTemplateDetails(params.id);
540
- }
541
- // No need to resetStore here, as we want to keep the new template's details
542
- }, [params?.id, resetFormData, mobilePushActions]);
543
-
544
- // Add a useEffect to reset form data only when switching from create mode to edit mode
545
- useEffect(() => {
546
- const currentMode = { id: params?.id, isFullMode };
547
- const prevMode = prevModeRef.current;
548
- const isEditModeNow = !!(params?.id && isFullMode);
549
- const wasCreateMode = !(prevMode.id && prevMode.isFullMode);
550
- const isModeChange = prevMode.id !== currentMode.id || prevMode.isFullMode !== currentMode.isFullMode;
551
-
552
- // Only reset if we're switching from create mode to edit mode
553
- if (isModeChange && isEditModeNow && wasCreateMode) {
554
- resetFormData();
555
- }
556
-
557
- prevModeRef.current = currentMode;
558
- }, [params?.id, isFullMode, resetFormData]);
559
-
560
- // Add a useEffect to reset dataPopulated when switching templates
561
- useEffect(() => {
562
- // setDataPopulated(false); // Removed
563
- }, [params?.id]);
564
-
565
- // Data population useEffect - ONLY for edit mode
566
- useEffect(() => {
567
- if (!isEditMode) {
568
- return;
569
- }
570
-
571
- // Do NOT reset form state here; only populate data
572
- // resetFormData();
573
-
574
- const { name = "", versions = {} } = editData?.templateDetails || {};
575
- const editContent = versions?.base || {};
576
-
577
- // If templateDetails exist but content is empty/invalid, hide spinner
578
- if (editData?.templateDetails && isEmpty(editContent)) {
579
- setSpin(false);
580
- return;
581
- }
582
-
583
- if (editContent && !isEmpty(editContent)) {
584
- // Populate data immediately
585
- setTemplateName(name);
586
- // Process Android content
587
- const androidContentType = editContent?.ANDROID;
588
- if (!isEmpty(androidContentType)) {
589
- const {
590
- title: androidTitle = "",
591
- message: androidMessage = "",
592
- ctas: androidCta = [],
593
- expandableDetails: androidExpandableDetails = {},
594
- image: androidImage = "",
595
- cta: androidMainCta = null, // This is the main notification body CTA
596
- } = androidContentType || {};
597
-
598
- // Only destructure androidExpandableDetails here
599
- const { style: androidStyle, ctas: androidCtas = [], image: androidExpandableImage = "" } = androidExpandableDetails || {};
600
- // In iOS content extraction, use unique names:
601
-
602
- // Determine media type based on style and available content
603
- let androidMediaType = NONE;
604
- let androidImageSrc = "";
605
- let androidVideoSrc = "";
606
- let androidVideoPreview = "";
607
- let carouselDataFromEdit = [];
608
-
609
- if (androidStyle === BIG_PICTURE) {
610
- androidMediaType = IMAGE;
611
- androidImageSrc = androidExpandableImage || androidImage;
612
- } else if (androidStyle === BIG_TEXT) {
613
- androidMediaType = NONE;
614
- } else if (androidStyle === MANUAL_CAROUSEL) {
615
- androidMediaType = CAROUSEL;
616
- }
617
-
618
- // Handle video/GIF content from expandableDetails.media array (new format)
619
- if (androidExpandableDetails?.media && Array.isArray(androidExpandableDetails.media) && androidExpandableDetails.media.length > 0) {
620
- const mediaItem = androidExpandableDetails.media[0];
621
- if (mediaItem.type === VIDEO) {
622
- // Distinguish between actual video and GIF based on URL extension
623
- if (mediaItem.url && mediaItem.url.toLowerCase().includes('.gif')) {
624
- androidMediaType = GIF;
625
- } else {
626
- androidMediaType = VIDEO;
627
- }
628
- androidVideoSrc = mediaItem.url;
629
- androidVideoPreview = mediaItem.url;
630
- } else if (mediaItem.type === GIF) {
631
- androidMediaType = GIF;
632
- androidVideoSrc = mediaItem.url;
633
- androidVideoPreview = mediaItem.url;
634
- }
635
- }
636
- // Handle video content if present in image field (legacy format)
637
- else if (androidImage && (androidImage.includes('.mp4') || androidImage.includes('.mov'))) {
638
- androidMediaType = VIDEO;
639
- androidVideoSrc = androidImage;
640
- androidVideoPreview = androidImage; // Use same for preview
641
- }
642
- // Handle GIF content if present in image field (legacy format)
643
- else if (androidImage && androidImage.toLowerCase().includes('.gif')) {
644
- androidMediaType = GIF;
645
- androidVideoSrc = androidImage;
646
- androidVideoPreview = androidImage; // Use same for preview
647
- }
648
- // Handle carousel content from expandableDetails.carouselData
649
- else if ((androidExpandableDetails?.style === MANUAL_CAROUSEL || androidContentType?.type === CAROUSEL) && androidExpandableDetails?.carouselData) {
650
- androidMediaType = CAROUSEL;
651
- // Process carousel data from editData
652
- carouselDataFromEdit = androidExpandableDetails.carouselData.map((card) => ({
653
- mediaType: card.mediaType || IMAGE.toLowerCase(),
654
- imageUrl: card.imageUrl || '',
655
- videoSrc: card.videoSrc || '',
656
- buttons: card.buttons ? card.buttons.map((button) => {
657
- // Extract deep link keys from deepLinkValue URL for carousel buttons
658
- let deepLinkKeys = [];
659
- let matchedDeepLinkValue = button.deepLinkValue || "";
660
-
661
- if (button.linkType === DEEP_LINK && button.deepLinkValue) {
662
- // Parse deep links directly from accountData to avoid timing issues
663
- let availableDeepLinks = [];
664
- if (accountData?.configs?.deeplink) {
665
- try {
666
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
667
- if (Array.isArray(parsedDeepLinks)) {
668
- availableDeepLinks = parsedDeepLinks.map((link) => ({
669
- label: link?.name,
670
- value: link?.link,
671
- title: link?.link,
672
- keys: link?.keys,
673
- }));
674
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
675
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
676
- label: link?.name,
677
- value: link?.link,
678
- title: link?.link,
679
- keys: link?.keys,
680
- }));
681
- }
682
- } catch (error) {
683
- console.error("[MobilePushNew] Error parsing deeplinks for carousel button:", error);
684
- }
685
- }
686
-
687
- if (availableDeepLinks.length > 0) {
688
- // Remove query parameters from deepLinkValue to find the base deep link
689
- let baseDeepLinkValue = button.deepLinkValue;
690
- try {
691
- const url = new URL(button.deepLinkValue);
692
- baseDeepLinkValue = url.origin + url.pathname;
693
- } catch (error) {
694
- // If URL parsing fails, try to remove query parameters manually
695
- const queryIndex = button.deepLinkValue.indexOf('?');
696
- if (queryIndex !== -1) {
697
- baseDeepLinkValue = button.deepLinkValue.substring(0, queryIndex);
698
- }
699
- }
700
-
701
- // Find matching deep link by comparing with the base deep link value
702
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseDeepLinkValue);
703
- if (matchingDeepLink) {
704
- matchedDeepLinkValue = matchingDeepLink.value;
705
- } else {
706
- // If no exact match, try to find by partial match
707
- const partialMatch = availableDeepLinks.find((link) =>
708
- button.deepLinkValue.includes(link.value)
709
- || link.value.includes(baseDeepLinkValue)
710
- );
711
- if (partialMatch) {
712
- matchedDeepLinkValue = partialMatch.value;
713
- }
714
- }
715
- }
716
-
717
- // Extract deep link keys from the original deepLinkValue URL
718
- try {
719
- const url = new URL(button.deepLinkValue);
720
- const extractedKeys = [];
721
- url.searchParams.forEach((value, key) => {
722
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
723
- extractedKeys.push(key);
724
- }
725
- });
726
- if (extractedKeys.length > 0) {
727
- deepLinkKeys = extractedKeys;
728
- }
729
- } catch (error) {
730
- // If URL parsing fails, try manual extraction
731
- const url = button.deepLinkValue;
732
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
733
- if (queryMatch) {
734
- const extractedKeys = [];
735
- queryMatch.forEach((param) => {
736
- const [key, value] = param.replace(/^[?&]/, '').split('=');
737
- if (key === value) { // Only extract keys where value equals key
738
- extractedKeys.push(key);
739
- }
740
- });
741
- if (extractedKeys.length > 0) {
742
- deepLinkKeys = extractedKeys;
743
- }
744
- }
745
- }
746
- }
747
-
748
- return {
749
- ...button,
750
- deepLinkValue: matchedDeepLinkValue,
751
- deepLinkKeys: deepLinkKeys,
752
- };
753
- }) : [],
754
- }));
755
-
756
- // Initialize carousel images in Redux state for proper display in uploaders
757
- if (carouselDataFromEdit.length > 0) {
758
- const firstCard = carouselDataFromEdit[0];
759
- if (firstCard.mediaType === 'image' && firstCard.imageUrl) {
760
- // Set the first carousel image in Redux state for the uploader to recognize
761
- setUpdateMpushImageSrc(firstCard.imageUrl, 0, CAROUSEL);
762
- }
763
- }
764
- }
765
-
766
- let androidButtonsValue = [];
767
- if (Array.isArray(androidCtas)) {
768
- androidButtonsValue = androidCtas;
769
- } else if (Array.isArray(androidCta)) {
770
- androidButtonsValue = androidCta;
771
- }
772
- const androidContentData = {
773
- title: androidTitle,
774
- message: androidMessage,
775
- mediaType: androidMediaType,
776
- buttons: androidButtonsValue,
777
- actionOnClick: false,
778
- linkType: DEEP_LINK,
779
- deepLinkValue: "",
780
- deepLinkKeysValue: [], // Initialize deep link keys value
781
- externalLinkValue: "",
782
- imageSrc: androidImageSrc,
783
- videoSrc: androidVideoSrc,
784
- videoPreview: androidVideoPreview,
785
- carouselData: carouselDataFromEdit, // Initialize carousel data
786
- };
787
- // Set image/video sources for upload state with platform isolation
788
- if (androidImageSrc) {
789
- setUpdateMpushImageSrc(androidImageSrc, 0, IMAGE);
790
- }
791
-
792
- // Apply exact same simple pattern as images for videos/GIFs
793
- if (androidVideoSrc) {
794
- setUpdateMpushVideoSrc(0, {
795
- videoSrc: androidVideoSrc,
796
- previewUrl: androidVideoPreview,
797
- }, true); // isInitialization = true
798
- }
799
-
800
- // Check for main notification body CTA (actionOnClick functionality)
801
- if (androidMainCta?.actionLink) {
802
- androidContentData.actionOnClick = true;
803
-
804
- // Determine link type based on saved CTA type (prioritize saved type over URL pattern)
805
- if (androidMainCta.type === EXTERNAL_URL) {
806
- androidContentData.linkType = EXTERNAL_LINK;
807
- androidContentData.externalLinkValue = androidMainCta.actionLink;
808
- androidContentData.deepLinkValue = "";
809
- } else if (androidMainCta.type === DEEP_LINK) {
810
- androidContentData.linkType = DEEP_LINK;
811
-
812
- // Find the matching deep link from the available options
813
- let matchedDeepLinkValue = "";
814
- if (androidMainCta.actionLink) {
815
- // Parse deep links directly from accountData to avoid timing issues
816
- let availableDeepLinks = [];
817
- if (accountData?.configs?.deeplink) {
818
- try {
819
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
820
- if (Array.isArray(parsedDeepLinks)) {
821
- availableDeepLinks = parsedDeepLinks.map((link) => ({
822
- label: link?.name,
823
- value: link?.link,
824
- title: link?.link,
825
- keys: link?.keys,
826
- }));
827
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
828
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
829
- label: link?.name,
830
- value: link?.link,
831
- title: link?.link,
832
- keys: link?.keys,
833
- }));
834
- }
835
- } catch (error) {
836
- console.error("[MobilePushNew] Error parsing deeplinks for main CTA:", error);
837
- }
838
- }
839
-
840
- if (availableDeepLinks.length > 0) {
841
- // Remove query parameters from actionLink to find the base deep link
842
- let baseActionLink = androidMainCta.actionLink;
843
- try {
844
- const url = new URL(androidMainCta.actionLink);
845
- baseActionLink = url.origin + url.pathname;
846
- } catch (error) {
847
- // If URL parsing fails, try to remove query parameters manually
848
- const queryIndex = androidMainCta.actionLink.indexOf('?');
849
- if (queryIndex !== -1) {
850
- baseActionLink = androidMainCta.actionLink.substring(0, queryIndex);
851
- }
852
- }
853
-
854
- // Find matching deep link by comparing with the base action link
855
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseActionLink);
856
- if (matchingDeepLink) {
857
- matchedDeepLinkValue = matchingDeepLink.value;
858
- } else {
859
- // If no exact match, try to find by partial match
860
- const partialMatch = availableDeepLinks.find((link) =>
861
- androidMainCta.actionLink.includes(link.value)
862
- || link.value.includes(baseActionLink)
863
- );
864
- if (partialMatch) {
865
- matchedDeepLinkValue = partialMatch.value;
866
- }
867
- }
868
- }
869
- }
870
-
871
- androidContentData.deepLinkValue = matchedDeepLinkValue;
872
-
873
- // Handle deep link keys - could be string or array
874
- const { deepLinkKeys } = androidMainCta;
875
- let deepLinkKeysValue = [];
876
-
877
- // First try to get from main CTA
878
- if (Array.isArray(deepLinkKeys)) {
879
- deepLinkKeysValue = deepLinkKeys;
880
- } else if (typeof deepLinkKeys === 'string' && deepLinkKeys) {
881
- deepLinkKeysValue = [deepLinkKeys];
882
- }
883
-
884
- // If not found in main CTA, try to get from custom array
885
- if (deepLinkKeysValue.length === 0 && androidContentType?.custom && Array.isArray(androidContentType.custom)) {
886
- deepLinkKeysValue = androidContentType.custom
887
- .filter((item) => item.key && item.value)
888
- .map((item) => item.key);
889
- }
890
-
891
- // If still not found, try to extract from action link URL
892
- if (deepLinkKeysValue.length === 0 && androidMainCta?.actionLink) {
893
- try {
894
- const url = new URL(androidMainCta.actionLink);
895
- const extractedKeys = [];
896
- url.searchParams.forEach((value, key) => {
897
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
898
- extractedKeys.push(key);
899
- }
900
- });
901
- if (extractedKeys.length > 0) {
902
- deepLinkKeysValue = extractedKeys;
903
- }
904
- } catch (error) {
905
- // If URL parsing fails, try manual extraction
906
- const url = androidMainCta.actionLink;
907
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
908
- if (queryMatch) {
909
- const extractedKeys = [];
910
- queryMatch.forEach((param) => {
911
- const [key, value] = param.replace(/^[?&]/, '').split('=');
912
- if (key === value) { // Only extract keys where value equals key
913
- extractedKeys.push(key);
914
- }
915
- });
916
- if (extractedKeys.length > 0) {
917
- deepLinkKeysValue = extractedKeys;
918
- }
919
- }
920
- }
921
- }
922
-
923
- androidContentData.deepLinkKeysValue = deepLinkKeysValue;
924
- androidContentData.externalLinkValue = "";
925
- } else if (androidMainCta.actionLink.startsWith('http://') || androidMainCta.actionLink.startsWith('https://')) {
926
- androidContentData.linkType = EXTERNAL_LINK;
927
- androidContentData.externalLinkValue = androidMainCta.actionLink;
928
- androidContentData.deepLinkValue = "";
929
- } else {
930
- androidContentData.linkType = DEEP_LINK;
931
- androidContentData.deepLinkValue = androidMainCta.actionLink;
932
- androidContentData.externalLinkValue = "";
933
- }
934
- } else {
935
- // If no CTA data, ensure both link values are empty
936
- androidContentData.deepLinkValue = "";
937
- androidContentData.externalLinkValue = "";
938
- }
939
-
940
- // Set Android content after all deep link keys are processed
941
- setAndroidContent(androidContentData);
942
-
943
- // Set CTA data for buttons (from expandableDetails.ctas)
944
- const androidButtons = androidExpandableDetails?.ctas || [];
945
-
946
- if (androidButtons.length > 0) {
947
- const ctaDataFromAndroid = androidButtons.map((button, index) => {
948
- // Extract deep link keys from action link URL for button CTAs
949
- let deepLinkKeys = [];
950
- if (button.type === DEEP_LINK && button.actionLink) {
951
- // First try to get from button's deepLinkKeys property
952
- if (Array.isArray(button.deepLinkKeys)) {
953
- deepLinkKeys = button.deepLinkKeys;
954
- } else if (typeof button.deepLinkKeys === 'string' && button.deepLinkKeys) {
955
- deepLinkKeys = [button.deepLinkKeys];
956
- }
957
-
958
- // If not found in button, try to get from custom array
959
- if (deepLinkKeys.length === 0 && androidExpandableDetails?.custom && Array.isArray(androidExpandableDetails.custom)) {
960
- const customKeys = androidExpandableDetails.custom
961
- .filter((item) => item.key && item.value)
962
- .map((item) => item.key);
963
- if (customKeys.length > 0) {
964
- deepLinkKeys = customKeys;
965
- }
966
- }
967
-
968
- // If still not found, try to extract from action link URL
969
- if (deepLinkKeys.length === 0) {
970
- try {
971
- const url = new URL(button.actionLink);
972
- const extractedKeys = [];
973
- url.searchParams.forEach((value, key) => {
974
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
975
- extractedKeys.push(key);
976
- }
977
- });
978
- if (extractedKeys.length > 0) {
979
- deepLinkKeys = extractedKeys;
980
- }
981
- } catch (error) {
982
- // If URL parsing fails, try manual extraction
983
- const url = button.actionLink;
984
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
985
- if (queryMatch) {
986
- const extractedKeys = [];
987
- queryMatch.forEach((param) => {
988
- const [key, value] = param.replace(/^[?&]/, '').split('=');
989
- if (key === value) { // Only extract keys where value equals key
990
- extractedKeys.push(key);
991
- }
992
- });
993
- if (extractedKeys.length > 0) {
994
- deepLinkKeys = extractedKeys;
995
- }
996
- }
997
- }
998
- }
999
- }
1000
-
1001
- const ctaData = {
1002
- text: button.actionText || button.label || "",
1003
- index,
1004
- url: (() => {
1005
- // For deep link buttons, find the matching deep link value
1006
- if (button.type === DEEP_LINK && button.actionLink) {
1007
- // Parse deep links directly from accountData to avoid timing issues
1008
- let availableDeepLinks = [];
1009
- if (accountData?.configs?.deeplink) {
1010
- try {
1011
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
1012
- if (Array.isArray(parsedDeepLinks)) {
1013
- availableDeepLinks = parsedDeepLinks.map((link) => ({
1014
- label: link?.name,
1015
- value: link?.link,
1016
- title: link?.link,
1017
- keys: link?.keys,
1018
- }));
1019
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
1020
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
1021
- label: link?.name,
1022
- value: link?.link,
1023
- title: link?.link,
1024
- keys: link?.keys,
1025
- }));
1026
- }
1027
- } catch (error) {
1028
- console.error("[MobilePushNew] Error parsing deeplinks for iOS button CTA:", error);
1029
- }
1030
- }
1031
-
1032
- if (availableDeepLinks.length > 0) {
1033
- // Remove query parameters from actionLink to find the base deep link
1034
- let baseActionLink = button.actionLink;
1035
- try {
1036
- const url = new URL(button.actionLink);
1037
- baseActionLink = url.origin + url.pathname;
1038
- } catch (error) {
1039
- // If URL parsing fails, try to remove query parameters manually
1040
- const queryIndex = button.actionLink.indexOf('?');
1041
- if (queryIndex !== -1) {
1042
- baseActionLink = button.actionLink.substring(0, queryIndex);
1043
- }
1044
- }
1045
-
1046
- // Find matching deep link by comparing with the base action link
1047
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseActionLink);
1048
- if (matchingDeepLink) {
1049
- return matchingDeepLink.value;
1050
- } else {
1051
- // If no exact match, try to find by partial match
1052
- const partialMatch = availableDeepLinks.find((link) =>
1053
- button.actionLink.includes(link.value)
1054
- || link.value.includes(baseActionLink)
1055
- );
1056
- if (partialMatch) {
1057
- return partialMatch.value;
1058
- }
1059
- }
1060
- }
1061
- }
1062
- return button.actionLink || "";
1063
- })(),
1064
- urlType: button.type || DEEP_LINK,
1065
- deepLinkKeys: deepLinkKeys,
1066
- ctaType: index === 0 ? PRIMARY : SECONDARY,
1067
- isSaved: true, // Mark as saved since it's loaded from backend
1068
- };
1069
-
1070
-
1071
-
1072
- return ctaData;
1073
- });
1074
- setCtaDataAndroid(ctaDataFromAndroid);
1075
- setPrimaryButtonAndroid(androidButtons.length >= 1);
1076
- setSecondaryButtonAndroid(androidButtons.length >= 2);
1077
- } else {
1078
- setCtaDataAndroid([]);
1079
- setPrimaryButtonAndroid(false);
1080
- setSecondaryButtonAndroid(false);
1081
- }
1082
- }
1083
-
1084
- // Process iOS content
1085
- const iosContentType = editContent?.IOS;
1086
- if (!isEmpty(iosContentType)) {
1087
- const {
1088
- title: iosTitle = "",
1089
- message: iosMessage = "",
1090
- ctas = [],
1091
- expandableDetails: iosExpandableDetails = {},
1092
- image: iosImage = "",
1093
- // Removed unused iosCta and iosMainCta variables
1094
- } = iosContentType || {};
1095
-
1096
- // Only destructure iosExpandableDetails here
1097
- const { style: iosStyle, image: iosExpandableImage = "" } = iosExpandableDetails || {};
1098
- // In iOS content extraction, use unique names:
1099
-
1100
- // Determine media type based on style and available content
1101
- let iosMediaType = NONE;
1102
- let iosImageSrc = "";
1103
- let iosVideoSrc = "";
1104
- let iosVideoPreview = "";
1105
- let carouselDataFromEdit = [];
1106
-
1107
- if (iosStyle === BIG_PICTURE) {
1108
- iosMediaType = IMAGE;
1109
- iosImageSrc = iosExpandableImage || iosImage;
1110
- } else if (iosStyle === BIG_TEXT) {
1111
- iosMediaType = NONE;
1112
- } else if (iosStyle === MANUAL_CAROUSEL) {
1113
- iosMediaType = CAROUSEL;
1114
- }
1115
-
1116
- // Handle video/GIF content from expandableDetails.media array (new format)
1117
- if (iosExpandableDetails?.media && Array.isArray(iosExpandableDetails.media) && iosExpandableDetails.media.length > 0) {
1118
- const mediaItem = iosExpandableDetails.media[0];
1119
- if (mediaItem.type === VIDEO) {
1120
- // Distinguish between actual video and GIF based on URL extension
1121
- if (mediaItem.url && mediaItem.url.toLowerCase().includes('.gif')) {
1122
- iosMediaType = GIF;
1123
- } else {
1124
- iosMediaType = VIDEO;
1125
- }
1126
- iosVideoSrc = mediaItem.url;
1127
- iosVideoPreview = mediaItem.url;
1128
- } else if (mediaItem.type === GIF) {
1129
- iosMediaType = GIF;
1130
- iosVideoSrc = mediaItem.url;
1131
- iosVideoPreview = mediaItem.url;
1132
- }
1133
- }
1134
- // Handle video content if present in image field (legacy format)
1135
- else if (iosImage && (iosImage.includes('.mp4') || iosImage.includes('.mov'))) {
1136
- iosMediaType = VIDEO;
1137
- iosVideoSrc = iosImage;
1138
- iosVideoPreview = iosImage; // Use same for preview
1139
- }
1140
- // Handle GIF content if present in image field (legacy format)
1141
- else if (iosImage && iosImage.toLowerCase().includes('.gif')) {
1142
- iosMediaType = GIF;
1143
- iosVideoSrc = iosImage;
1144
- iosVideoPreview = iosImage; // Use same for preview
1145
- }
1146
- // Handle carousel content from expandableDetails.carouselData
1147
- else if ((iosExpandableDetails?.style === MANUAL_CAROUSEL || iosContentType?.type === CAROUSEL) && iosExpandableDetails?.carouselData) {
1148
- iosMediaType = CAROUSEL;
1149
- // Process carousel data from editData
1150
- carouselDataFromEdit = iosExpandableDetails.carouselData.map((card) => ({
1151
- mediaType: card.mediaType || IMAGE.toLowerCase(),
1152
- imageUrl: card.imageUrl || '',
1153
- videoSrc: card.videoSrc || '',
1154
- buttons: card.buttons ? card.buttons.map((button) => {
1155
- // Extract deep link keys from deepLinkValue URL for carousel buttons
1156
- let deepLinkKeys = [];
1157
- let matchedDeepLinkValue = button.deepLinkValue || "";
1158
-
1159
- if (button.linkType === DEEP_LINK && button.deepLinkValue) {
1160
- // Parse deep links directly from accountData to avoid timing issues
1161
- let availableDeepLinks = [];
1162
- if (accountData?.configs?.deeplink) {
1163
- try {
1164
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
1165
- if (Array.isArray(parsedDeepLinks)) {
1166
- availableDeepLinks = parsedDeepLinks.map((link) => ({
1167
- label: link?.name,
1168
- value: link?.link,
1169
- title: link?.link,
1170
- keys: link?.keys,
1171
- }));
1172
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
1173
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
1174
- label: link?.name,
1175
- value: link?.link,
1176
- title: link?.link,
1177
- keys: link?.keys,
1178
- }));
1179
- }
1180
- } catch (error) {
1181
- console.error("[MobilePushNew] Error parsing deeplinks for carousel button:", error);
1182
- }
1183
- }
1184
-
1185
- if (availableDeepLinks.length > 0) {
1186
- // Remove query parameters from deepLinkValue to find the base deep link
1187
- let baseDeepLinkValue = button.deepLinkValue;
1188
- try {
1189
- const url = new URL(button.deepLinkValue);
1190
- baseDeepLinkValue = url.origin + url.pathname;
1191
- } catch (error) {
1192
- // If URL parsing fails, try to remove query parameters manually
1193
- const queryIndex = button.deepLinkValue.indexOf('?');
1194
- if (queryIndex !== -1) {
1195
- baseDeepLinkValue = button.deepLinkValue.substring(0, queryIndex);
1196
- }
1197
- }
1198
-
1199
- // Find matching deep link by comparing with the base deep link value
1200
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseDeepLinkValue);
1201
- if (matchingDeepLink) {
1202
- matchedDeepLinkValue = matchingDeepLink.value;
1203
- } else {
1204
- // If no exact match, try to find by partial match
1205
- const partialMatch = availableDeepLinks.find((link) =>
1206
- button.deepLinkValue.includes(link.value)
1207
- || link.value.includes(baseDeepLinkValue)
1208
- );
1209
- if (partialMatch) {
1210
- matchedDeepLinkValue = partialMatch.value;
1211
- }
1212
- }
1213
- }
1214
-
1215
- // Extract deep link keys from the original deepLinkValue URL
1216
- try {
1217
- const url = new URL(button.deepLinkValue);
1218
- const extractedKeys = [];
1219
- url.searchParams.forEach((value, key) => {
1220
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
1221
- extractedKeys.push(key);
1222
- }
1223
- });
1224
- if (extractedKeys.length > 0) {
1225
- deepLinkKeys = extractedKeys;
1226
- }
1227
- } catch (error) {
1228
- // If URL parsing fails, try manual extraction
1229
- const url = button.deepLinkValue;
1230
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
1231
- if (queryMatch) {
1232
- const extractedKeys = [];
1233
- queryMatch.forEach((param) => {
1234
- const [key, value] = param.replace(/^[?&]/, '').split('=');
1235
- if (key === value) { // Only extract keys where value equals key
1236
- extractedKeys.push(key);
1237
- }
1238
- });
1239
- if (extractedKeys.length > 0) {
1240
- deepLinkKeys = extractedKeys;
1241
- }
1242
- }
1243
- }
1244
- }
1245
-
1246
- return {
1247
- ...button,
1248
- deepLinkValue: matchedDeepLinkValue,
1249
- deepLinkKeys: deepLinkKeys,
1250
- };
1251
- }) : [],
1252
- }));
1253
-
1254
- // Initialize carousel images in Redux state for proper display in uploaders
1255
- if (carouselDataFromEdit.length > 0) {
1256
- const firstCard = carouselDataFromEdit[0];
1257
- if (firstCard.mediaType === 'image' && firstCard.imageUrl) {
1258
- // Set the first carousel image in Redux state for the uploader to recognize
1259
- setUpdateMpushImageSrc(firstCard.imageUrl, 1, CAROUSEL);
1260
- }
1261
- }
1262
- }
1263
-
1264
- let iosButtonsValue = [];
1265
- if (Array.isArray(ctas)) {
1266
- iosButtonsValue = ctas;
1267
- }
1268
- const iosContentData = {
1269
- title: iosTitle,
1270
- message: iosMessage,
1271
- mediaType: iosMediaType,
1272
- buttons: iosButtonsValue,
1273
- actionOnClick: false,
1274
- linkType: DEEP_LINK,
1275
- deepLinkValue: "",
1276
- deepLinkKeysValue: [],
1277
- externalLinkValue: "",
1278
- imageSrc: iosImageSrc,
1279
- videoSrc: iosVideoSrc,
1280
- videoPreview: iosVideoPreview,
1281
- carouselData: carouselDataFromEdit, // Initialize carousel data
1282
- };
1283
-
1284
- // Check for deep link keys in custom array for iOS
1285
- if (iosContentType?.custom && Array.isArray(iosContentType.custom)) {
1286
- const deepLinkKeysFromCustom = iosContentType.custom
1287
- .filter((item) => item.key && item.value)
1288
- .map((item) => item.key);
1289
- if (deepLinkKeysFromCustom.length > 0) {
1290
- iosContentData.deepLinkKeysValue = deepLinkKeysFromCustom;
1291
- }
1292
- }
1293
-
1294
- // For iOS, also check button CTAs for deep link keys in URLs
1295
- if (iosContentData.deepLinkKeysValue.length === 0 && iosExpandableDetails?.ctas) {
1296
- for (const cta of iosExpandableDetails.ctas) {
1297
- if (cta.type === DEEP_LINK && cta.actionLink) {
1298
- try {
1299
- const url = new URL(cta.actionLink);
1300
- const extractedKeys = [];
1301
- url.searchParams.forEach((value, key) => {
1302
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
1303
- extractedKeys.push(key);
1304
- }
1305
- });
1306
- if (extractedKeys.length > 0) {
1307
- iosContentData.deepLinkKeysValue = extractedKeys;
1308
- break; // Use the first CTA with deep link keys
1309
- }
1310
- } catch (error) {
1311
- // If URL parsing fails, try manual extraction
1312
- const url = cta.actionLink;
1313
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
1314
- if (queryMatch) {
1315
- const extractedKeys = [];
1316
- queryMatch.forEach((param) => {
1317
- const [key, value] = param.replace(/^[?&]/, '').split('=');
1318
- if (key === value) { // Only extract keys where value equals key
1319
- extractedKeys.push(key);
1320
- }
1321
- });
1322
- if (extractedKeys.length > 0) {
1323
- iosContentData.deepLinkKeysValue = extractedKeys;
1324
- break; // Use the first CTA with deep link keys
1325
- }
1326
- }
1327
- }
1328
- }
1329
- }
1330
- }
1331
-
1332
- setIosContent(iosContentData);
1333
- // Set image/video sources for upload state with platform isolation
1334
- if (iosImageSrc) {
1335
- setUpdateMpushImageSrc(iosImageSrc, 1, IMAGE);
1336
- }
1337
-
1338
- // Apply exact same simple pattern as images for videos/GIFs
1339
- if (iosVideoSrc) {
1340
- setUpdateMpushVideoSrc(1, {
1341
- videoSrc: iosVideoSrc,
1342
- previewUrl: iosVideoPreview,
1343
- }, true); // isInitialization = true
1344
- }
1345
-
1346
- // Check for main notification body CTA (actionOnClick functionality)
1347
- // Removed all iosMainCta references (was not defined or used)
1348
-
1349
- // Set CTA data for buttons (from expandableDetails.ctas)
1350
- const iosButtons = iosExpandableDetails?.ctas || [];
1351
- if (iosButtons.length > 0) {
1352
- const ctaDataFromIos = iosButtons.map((button, index) => {
1353
- // Extract deep link keys from action link URL for button CTAs
1354
- let deepLinkKeys = [];
1355
- if (button.type === DEEP_LINK && button.actionLink) {
1356
- try {
1357
- const url = new URL(button.actionLink);
1358
- const extractedKeys = [];
1359
- url.searchParams.forEach((value, key) => {
1360
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
1361
- extractedKeys.push(key);
1362
- }
1363
- });
1364
- if (extractedKeys.length > 0) {
1365
- deepLinkKeys = extractedKeys;
1366
- }
1367
- } catch (error) {
1368
- // If URL parsing fails, try manual extraction
1369
- const url = button.actionLink;
1370
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
1371
- if (queryMatch) {
1372
- const extractedKeys = [];
1373
- queryMatch.forEach((param) => {
1374
- const [key, value] = param.replace(/^[?&]/, '').split('=');
1375
- if (key === value) { // Only extract keys where value equals key
1376
- extractedKeys.push(key);
1377
- }
1378
- });
1379
- if (extractedKeys.length > 0) {
1380
- deepLinkKeys = extractedKeys;
1381
- }
1382
- }
1383
- }
1384
- }
1385
-
1386
- return {
1387
- text: button.actionText || button.label || "",
1388
- index,
1389
- url: (() => {
1390
- // For deep link buttons, find the matching deep link value
1391
- if (button.type === DEEP_LINK && button.actionLink) {
1392
- // Parse deep links directly from accountData to avoid timing issues
1393
- let availableDeepLinks = [];
1394
- if (accountData?.configs?.deeplink) {
1395
- try {
1396
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
1397
- if (Array.isArray(parsedDeepLinks)) {
1398
- availableDeepLinks = parsedDeepLinks.map((link) => ({
1399
- label: link?.name,
1400
- value: link?.link,
1401
- title: link?.link,
1402
- keys: link?.keys,
1403
- }));
1404
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
1405
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
1406
- label: link?.name,
1407
- value: link?.link,
1408
- title: link?.link,
1409
- keys: link?.keys,
1410
- }));
1411
- }
1412
- } catch (error) {
1413
- console.error("[MobilePushNew] Error parsing deeplinks for iOS button CTA:", error);
1414
- }
1415
- }
1416
-
1417
- if (availableDeepLinks.length > 0) {
1418
- // Remove query parameters from actionLink to find the base deep link
1419
- let baseActionLink = button.actionLink;
1420
- try {
1421
- const url = new URL(button.actionLink);
1422
- baseActionLink = url.origin + url.pathname;
1423
- } catch (error) {
1424
- // If URL parsing fails, try to remove query parameters manually
1425
- const queryIndex = button.actionLink.indexOf('?');
1426
- if (queryIndex !== -1) {
1427
- baseActionLink = button.actionLink.substring(0, queryIndex);
1428
- }
1429
- }
1430
-
1431
- // Find matching deep link by comparing with the base action link
1432
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseActionLink);
1433
- if (matchingDeepLink) {
1434
- return matchingDeepLink.value;
1435
- } else {
1436
- // If no exact match, try to find by partial match
1437
- const partialMatch = availableDeepLinks.find((link) =>
1438
- button.actionLink.includes(link.value)
1439
- || link.value.includes(baseActionLink)
1440
- );
1441
- if (partialMatch) {
1442
- return partialMatch.value;
1443
- }
1444
- }
1445
- }
1446
- }
1447
- return button.actionLink || "";
1448
- })(),
1449
- urlType: button.type || DEEP_LINK,
1450
- deepLinkKeys: deepLinkKeys,
1451
- ctaType: index === 0 ? PRIMARY : SECONDARY,
1452
- isSaved: true, // Mark as saved since it's loaded from backend
1453
- };
1454
- });
1455
- setCtaDataIos(ctaDataFromIos);
1456
- setPrimaryButtonIos(iosButtons.length >= 1);
1457
- setSecondaryButtonIos(iosButtons.length >= 2);
1458
- } else {
1459
- setCtaDataIos([]);
1460
- setPrimaryButtonIos(false);
1461
- setSecondaryButtonIos(false);
1462
- }
1463
- }
1464
-
1465
- // Mark initial load as complete after data population and hide spinner
1466
- // setIsInitialLoad(false); // Removed
1467
- // setDataPopulated(true); // Removed
1468
- setSpin(false); // Hide spinner only after all state is populated
1469
- }
1470
- }, [editData?.templateDetails, isEditMode, params?.id]);
1471
-
1472
- // Data population useEffect - for library mode (not full mode) using templateData
1473
- useEffect(() => {
1474
- if (isFullMode || !templateData || isEmpty(templateData)) {
1475
- return;
1476
- }
1477
-
1478
- // Do NOT reset form state here; only populate data
1479
- // resetFormData();
1480
-
1481
- // templateData is expected to have a similar structure as editData.templateDetails
1482
- const { name = "", versions = {} } = templateData || {};
1483
- const templateContent = versions?.base || {};
1484
-
1485
- if (isEmpty(templateContent)) {
1486
- setSpin(false);
1487
- return;
1488
- }
1489
-
1490
- setTemplateName(name);
1491
- // Process Android content
1492
- const androidContentType = templateContent?.ANDROID;
1493
- if (!isEmpty(androidContentType)) {
1494
- const {
1495
- title: androidTitle = "",
1496
- message: androidMessage = "",
1497
- ctas: androidCta = [],
1498
- expandableDetails: androidExpandableDetails = {},
1499
- image: androidImage = "",
1500
- cta: androidMainCta = null,
1501
- } = androidContentType || {};
1502
- const { style: androidStyle, ctas: androidCtas = [], image: androidExpandableImage = "" } = androidExpandableDetails || {};
1503
- let androidMediaType = NONE;
1504
- let androidImageSrc = "";
1505
- let androidVideoSrc = "";
1506
- let androidVideoPreview = "";
1507
- let carouselDataFromTemplate = [];
1508
- if (androidStyle === BIG_PICTURE) {
1509
- androidMediaType = IMAGE;
1510
- androidImageSrc = androidExpandableImage || androidImage;
1511
- } else if (androidStyle === BIG_TEXT) {
1512
- androidMediaType = NONE;
1513
- } else if (androidStyle === MANUAL_CAROUSEL) {
1514
- androidMediaType = CAROUSEL;
1515
- }
1516
- if (androidExpandableDetails?.media && Array.isArray(androidExpandableDetails.media) && androidExpandableDetails.media.length > 0) {
1517
- const mediaItem = androidExpandableDetails.media[0];
1518
- if (mediaItem.type === VIDEO) {
1519
- if (mediaItem.url && mediaItem.url.toLowerCase().includes('.gif')) {
1520
- androidMediaType = GIF;
1521
- } else {
1522
- androidMediaType = VIDEO;
1523
- }
1524
- androidVideoSrc = mediaItem.url;
1525
- androidVideoPreview = mediaItem.url;
1526
- } else if (mediaItem.type === GIF) {
1527
- androidMediaType = GIF;
1528
- androidVideoSrc = mediaItem.url;
1529
- androidVideoPreview = mediaItem.url;
1530
- }
1531
- } else if (androidImage && (androidImage.includes('.mp4') || androidImage.includes('.mov'))) {
1532
- androidMediaType = VIDEO;
1533
- androidVideoSrc = androidImage;
1534
- androidVideoPreview = androidImage;
1535
- } else if (androidImage && androidImage.toLowerCase().includes('.gif')) {
1536
- androidMediaType = GIF;
1537
- androidVideoSrc = androidImage;
1538
- androidVideoPreview = androidImage;
1539
- } else if ((androidExpandableDetails?.style === MANUAL_CAROUSEL || androidContentType?.type === CAROUSEL) && androidExpandableDetails?.carouselData) {
1540
- androidMediaType = CAROUSEL;
1541
- carouselDataFromTemplate = androidExpandableDetails.carouselData.map((card) => ({
1542
- mediaType: card.mediaType || IMAGE.toLowerCase(),
1543
- imageUrl: card.imageUrl || '',
1544
- videoSrc: card.videoSrc || '',
1545
- buttons: card.buttons ? card.buttons.map((button) => {
1546
- // Extract deep link keys from deepLinkValue URL for carousel buttons
1547
- let deepLinkKeys = [];
1548
- let matchedDeepLinkValue = button.deepLinkValue || "";
1549
-
1550
- if (button.linkType === DEEP_LINK && button.deepLinkValue) {
1551
- // Parse deep links directly from accountData to avoid timing issues
1552
- let availableDeepLinks = [];
1553
- if (accountData?.configs?.deeplink) {
1554
- try {
1555
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
1556
- if (Array.isArray(parsedDeepLinks)) {
1557
- availableDeepLinks = parsedDeepLinks.map((link) => ({
1558
- label: link?.name,
1559
- value: link?.link,
1560
- title: link?.link,
1561
- keys: link?.keys,
1562
- }));
1563
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
1564
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
1565
- label: link?.name,
1566
- value: link?.link,
1567
- title: link?.link,
1568
- keys: link?.keys,
1569
- }));
1570
- }
1571
- } catch (error) {
1572
- console.error("[MobilePushNew] Error parsing deeplinks for library mode carousel button:", error);
1573
- }
1574
- }
1575
-
1576
- if (availableDeepLinks.length > 0) {
1577
- // Remove query parameters from deepLinkValue to find the base deep link
1578
- let baseDeepLinkValue = button.deepLinkValue;
1579
- try {
1580
- const url = new URL(button.deepLinkValue);
1581
- baseDeepLinkValue = url.origin + url.pathname;
1582
- } catch (error) {
1583
- // If URL parsing fails, try to remove query parameters manually
1584
- const queryIndex = button.deepLinkValue.indexOf('?');
1585
- if (queryIndex !== -1) {
1586
- baseDeepLinkValue = button.deepLinkValue.substring(0, queryIndex);
1587
- }
1588
- }
1589
-
1590
- // Find matching deep link by comparing with the base deep link value
1591
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseDeepLinkValue);
1592
- if (matchingDeepLink) {
1593
- matchedDeepLinkValue = matchingDeepLink.value;
1594
- } else {
1595
- // If no exact match, try to find by partial match
1596
- const partialMatch = availableDeepLinks.find((link) =>
1597
- button.deepLinkValue.includes(link.value)
1598
- || link.value.includes(baseDeepLinkValue)
1599
- );
1600
- if (partialMatch) {
1601
- matchedDeepLinkValue = partialMatch.value;
1602
- }
1603
- }
1604
- }
1605
-
1606
- // Extract deep link keys from the original deepLinkValue URL
1607
- try {
1608
- const url = new URL(button.deepLinkValue);
1609
- const extractedKeys = [];
1610
- url.searchParams.forEach((value, key) => {
1611
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
1612
- extractedKeys.push(key);
1613
- }
1614
- });
1615
- if (extractedKeys.length > 0) {
1616
- deepLinkKeys = extractedKeys;
1617
- }
1618
- } catch (error) {
1619
- // If URL parsing fails, try manual extraction
1620
- const url = button.deepLinkValue;
1621
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
1622
- if (queryMatch) {
1623
- const extractedKeys = [];
1624
- queryMatch.forEach((param) => {
1625
- const [key, value] = param.replace(/^[?&]/, '').split('=');
1626
- if (key === value) { // Only extract keys where value equals key
1627
- extractedKeys.push(key);
1628
- }
1629
- });
1630
- if (extractedKeys.length > 0) {
1631
- deepLinkKeys = extractedKeys;
1632
- }
1633
- }
1634
- }
1635
- }
1636
-
1637
- return {
1638
- ...button,
1639
- deepLinkValue: matchedDeepLinkValue,
1640
- deepLinkKeys: deepLinkKeys,
1641
- };
1642
- }) : [],
1643
- }));
1644
-
1645
- // Initialize carousel images in Redux state for proper display in uploaders
1646
- if (carouselDataFromTemplate.length > 0) {
1647
- const firstCard = carouselDataFromTemplate[0];
1648
- if (firstCard.mediaType === 'image' && firstCard.imageUrl) {
1649
- // Set the first carousel image in Redux state for the uploader to recognize
1650
- setUpdateMpushImageSrc(firstCard.imageUrl, 0, CAROUSEL);
1651
- }
1652
- }
1653
- }
1654
- let androidButtonsValue = [];
1655
- if (Array.isArray(androidCtas)) {
1656
- androidButtonsValue = androidCtas;
1657
- } else if (Array.isArray(androidCta)) {
1658
- androidButtonsValue = androidCta;
1659
- }
1660
- const androidContentData = {
1661
- title: androidTitle,
1662
- message: androidMessage,
1663
- mediaType: androidMediaType,
1664
- buttons: androidButtonsValue,
1665
- actionOnClick: false,
1666
- linkType: DEEP_LINK,
1667
- deepLinkValue: "",
1668
- externalLinkValue: "",
1669
- imageSrc: androidImageSrc,
1670
- videoSrc: androidVideoSrc,
1671
- videoPreview: androidVideoPreview,
1672
- carouselData: carouselDataFromTemplate,
1673
- };
1674
- setAndroidContent(androidContentData);
1675
- if (androidImageSrc) {
1676
- setUpdateMpushImageSrc(androidImageSrc, 0, IMAGE);
1677
- }
1678
- if (androidVideoSrc) {
1679
- setUpdateMpushVideoSrc(0, {
1680
- videoSrc: androidVideoSrc,
1681
- previewUrl: androidVideoPreview,
1682
- }, true);
1683
- }
1684
- if (androidMainCta?.actionLink) {
1685
- androidContentData.actionOnClick = true;
1686
- if (androidMainCta.type === EXTERNAL_URL) {
1687
- androidContentData.linkType = EXTERNAL_LINK;
1688
- androidContentData.externalLinkValue = androidMainCta.actionLink;
1689
- androidContentData.deepLinkValue = "";
1690
- } else if (androidMainCta.type === DEEP_LINK) {
1691
- androidContentData.linkType = DEEP_LINK;
1692
-
1693
- // Find the matching deep link from the available options
1694
- let matchedDeepLinkValue = "";
1695
- if (androidMainCta.actionLink) {
1696
- // Parse deep links directly from accountData to avoid timing issues
1697
- let availableDeepLinks = [];
1698
- if (accountData?.configs?.deeplink) {
1699
- try {
1700
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
1701
- if (Array.isArray(parsedDeepLinks)) {
1702
- availableDeepLinks = parsedDeepLinks.map((link) => ({
1703
- label: link?.name,
1704
- value: link?.link,
1705
- title: link?.link,
1706
- keys: link?.keys,
1707
- }));
1708
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
1709
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
1710
- label: link?.name,
1711
- value: link?.link,
1712
- title: link?.link,
1713
- keys: link?.keys,
1714
- }));
1715
- }
1716
- } catch (error) {
1717
- console.error("[MobilePushNew] Error parsing deeplinks for library mode main CTA:", error);
1718
- }
1719
- }
1720
-
1721
- if (availableDeepLinks.length > 0) {
1722
- // Remove query parameters from actionLink to find the base deep link
1723
- let baseActionLink = androidMainCta.actionLink;
1724
- try {
1725
- const url = new URL(androidMainCta.actionLink);
1726
- baseActionLink = url.origin + url.pathname;
1727
- } catch (error) {
1728
- // If URL parsing fails, try to remove query parameters manually
1729
- const queryIndex = androidMainCta.actionLink.indexOf('?');
1730
- if (queryIndex !== -1) {
1731
- baseActionLink = androidMainCta.actionLink.substring(0, queryIndex);
1732
- }
1733
- }
1734
-
1735
- // Find matching deep link by comparing with the base action link
1736
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseActionLink);
1737
- if (matchingDeepLink) {
1738
- matchedDeepLinkValue = matchingDeepLink.value;
1739
- } else {
1740
- // If no exact match, try to find by partial match
1741
- const partialMatch = availableDeepLinks.find((link) =>
1742
- androidMainCta.actionLink.includes(link.value)
1743
- || link.value.includes(baseActionLink)
1744
- );
1745
- if (partialMatch) {
1746
- matchedDeepLinkValue = partialMatch.value;
1747
- }
1748
- }
1749
- }
1750
- }
1751
-
1752
- androidContentData.deepLinkValue = matchedDeepLinkValue;
1753
- androidContentData.externalLinkValue = "";
1754
-
1755
- // Extract deep link keys from action link URL for main CTA
1756
- let deepLinkKeysValue = [];
1757
- if (androidMainCta.actionLink) {
1758
- try {
1759
- const url = new URL(androidMainCta.actionLink);
1760
- const extractedKeys = [];
1761
- url.searchParams.forEach((value, key) => {
1762
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
1763
- extractedKeys.push(key);
1764
- }
1765
- });
1766
- if (extractedKeys.length > 0) {
1767
- deepLinkKeysValue = extractedKeys;
1768
- }
1769
- } catch (error) {
1770
- // If URL parsing fails, try manual extraction
1771
- const url = androidMainCta.actionLink;
1772
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
1773
- if (queryMatch) {
1774
- const extractedKeys = [];
1775
- queryMatch.forEach((param) => {
1776
- const [key, value] = param.replace(/^[?&]/, '').split('=');
1777
- if (key === value) { // Only extract keys where value equals key
1778
- extractedKeys.push(key);
1779
- }
1780
- });
1781
- if (extractedKeys.length > 0) {
1782
- deepLinkKeysValue = extractedKeys;
1783
- }
1784
- }
1785
- }
1786
- }
1787
- androidContentData.deepLinkKeysValue = deepLinkKeysValue;
1788
- } else if (androidMainCta.actionLink.startsWith('http://') || androidMainCta.actionLink.startsWith('https://')) {
1789
- androidContentData.linkType = EXTERNAL_LINK;
1790
- androidContentData.externalLinkValue = androidMainCta.actionLink;
1791
- androidContentData.deepLinkValue = "";
1792
- } else {
1793
- androidContentData.linkType = DEEP_LINK;
1794
- androidContentData.deepLinkValue = androidMainCta.actionLink;
1795
- androidContentData.externalLinkValue = "";
1796
- }
1797
- } else {
1798
- androidContentData.deepLinkValue = "";
1799
- androidContentData.externalLinkValue = "";
1800
- }
1801
- const androidButtons = androidExpandableDetails?.ctas || [];
1802
- if (androidButtons.length > 0) {
1803
- const ctaDataFromAndroid = androidButtons.map((button, index) => {
1804
- // Extract deep link keys from action link URL for button CTAs
1805
- let deepLinkKeys = [];
1806
- if (button.type === DEEP_LINK && button.actionLink) {
1807
- try {
1808
- const url = new URL(button.actionLink);
1809
- const extractedKeys = [];
1810
- url.searchParams.forEach((value, key) => {
1811
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
1812
- extractedKeys.push(key);
1813
- }
1814
- });
1815
- if (extractedKeys.length > 0) {
1816
- deepLinkKeys = extractedKeys;
1817
- }
1818
- } catch (error) {
1819
- // If URL parsing fails, try manual extraction
1820
- const url = button.actionLink;
1821
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
1822
- if (queryMatch) {
1823
- const extractedKeys = [];
1824
- queryMatch.forEach((param) => {
1825
- const [key, value] = param.replace(/^[?&]/, '').split('=');
1826
- if (key === value) { // Only extract keys where value equals key
1827
- extractedKeys.push(key);
1828
- }
1829
- });
1830
- if (extractedKeys.length > 0) {
1831
- deepLinkKeys = extractedKeys;
1832
- }
1833
- }
1834
- }
1835
- }
1836
-
1837
- return {
1838
- text: button.actionText || button.label || "",
1839
- index,
1840
- url: (() => {
1841
- // For deep link buttons, find the matching deep link value
1842
- if (button.type === DEEP_LINK && button.actionLink) {
1843
- // Parse deep links directly from accountData to avoid timing issues
1844
- let availableDeepLinks = [];
1845
- if (accountData?.configs?.deeplink) {
1846
- try {
1847
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
1848
- if (Array.isArray(parsedDeepLinks)) {
1849
- availableDeepLinks = parsedDeepLinks.map((link) => ({
1850
- label: link?.name,
1851
- value: link?.link,
1852
- title: link?.link,
1853
- keys: link?.keys,
1854
- }));
1855
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
1856
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
1857
- label: link?.name,
1858
- value: link?.link,
1859
- title: link?.link,
1860
- keys: link?.keys,
1861
- }));
1862
- }
1863
- } catch (error) {
1864
- console.error("[MobilePushNew] Error parsing deeplinks for library mode button CTA:", error);
1865
- }
1866
- }
1867
-
1868
- if (availableDeepLinks.length > 0) {
1869
- // Remove query parameters from actionLink to find the base deep link
1870
- let baseActionLink = button.actionLink;
1871
- try {
1872
- const url = new URL(button.actionLink);
1873
- baseActionLink = url.origin + url.pathname;
1874
- } catch (error) {
1875
- // If URL parsing fails, try to remove query parameters manually
1876
- const queryIndex = button.actionLink.indexOf('?');
1877
- if (queryIndex !== -1) {
1878
- baseActionLink = button.actionLink.substring(0, queryIndex);
1879
- }
1880
- }
1881
-
1882
- // Find matching deep link by comparing with the base action link
1883
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseActionLink);
1884
- if (matchingDeepLink) {
1885
- return matchingDeepLink.value;
1886
- } else {
1887
- // If no exact match, try to find by partial match
1888
- const partialMatch = availableDeepLinks.find((link) =>
1889
- button.actionLink.includes(link.value)
1890
- || link.value.includes(baseActionLink)
1891
- );
1892
- if (partialMatch) {
1893
- return partialMatch.value;
1894
- }
1895
- }
1896
- }
1897
- }
1898
- return button.actionLink || "";
1899
- })(),
1900
- urlType: button.type || DEEP_LINK,
1901
- deepLinkKeys: deepLinkKeys,
1902
- ctaType: index === 0 ? PRIMARY : SECONDARY,
1903
- isSaved: true,
1904
- };
1905
- });
1906
- setCtaDataAndroid(ctaDataFromAndroid);
1907
- setPrimaryButtonAndroid(androidButtons.length >= 1);
1908
- setSecondaryButtonAndroid(androidButtons.length >= 2);
1909
- } else {
1910
- setCtaDataAndroid([]);
1911
- setPrimaryButtonAndroid(false);
1912
- setSecondaryButtonAndroid(false);
1913
- }
1914
- }
1915
- // Process iOS content
1916
- const iosContentType = templateContent?.IOS;
1917
- if (!isEmpty(iosContentType)) {
1918
- const {
1919
- title: iosTitle = "",
1920
- message: iosMessage = "",
1921
- ctas = [],
1922
- expandableDetails: iosExpandableDetails = {},
1923
- image: iosImage = "",
1924
- } = iosContentType || {};
1925
- const { style: iosStyle, image: iosExpandableImage = "" } = iosExpandableDetails || {};
1926
- let iosMediaType = NONE;
1927
- let iosImageSrc = "";
1928
- let iosVideoSrc = "";
1929
- let iosVideoPreview = "";
1930
- let carouselDataFromTemplate = [];
1931
- if (iosStyle === BIG_PICTURE) {
1932
- iosMediaType = IMAGE;
1933
- iosImageSrc = iosExpandableImage || iosImage;
1934
- } else if (iosStyle === BIG_TEXT) {
1935
- iosMediaType = NONE;
1936
- } else if (iosStyle === MANUAL_CAROUSEL) {
1937
- iosMediaType = CAROUSEL;
1938
- }
1939
- if (iosExpandableDetails?.media && Array.isArray(iosExpandableDetails.media) && iosExpandableDetails.media.length > 0) {
1940
- const mediaItem = iosExpandableDetails.media[0];
1941
- if (mediaItem.type === VIDEO) {
1942
- if (mediaItem.url && mediaItem.url.toLowerCase().includes('.gif')) {
1943
- iosMediaType = GIF;
1944
- } else {
1945
- iosMediaType = VIDEO;
1946
- }
1947
- iosVideoSrc = mediaItem.url;
1948
- iosVideoPreview = mediaItem.url;
1949
- } else if (mediaItem.type === GIF) {
1950
- iosMediaType = GIF;
1951
- iosVideoSrc = mediaItem.url;
1952
- iosVideoPreview = mediaItem.url;
1953
- }
1954
- } else if (iosImage && (iosImage.includes('.mp4') || iosImage.includes('.mov'))) {
1955
- iosMediaType = VIDEO;
1956
- iosVideoSrc = iosImage;
1957
- iosVideoPreview = iosImage;
1958
- } else if (iosImage && iosImage.toLowerCase().includes('.gif')) {
1959
- iosMediaType = GIF;
1960
- iosVideoSrc = iosImage;
1961
- iosVideoPreview = iosImage;
1962
- } else if ((iosExpandableDetails?.style === MANUAL_CAROUSEL || iosContentType?.type === CAROUSEL) && iosExpandableDetails?.carouselData) {
1963
- iosMediaType = CAROUSEL;
1964
- carouselDataFromTemplate = iosExpandableDetails.carouselData.map((card) => ({
1965
- mediaType: card.mediaType || IMAGE.toLowerCase(),
1966
- imageUrl: card.imageUrl || '',
1967
- videoSrc: card.videoSrc || '',
1968
- buttons: card.buttons ? card.buttons.map((button) => {
1969
- // Extract deep link keys from deepLinkValue URL for carousel buttons
1970
- let deepLinkKeys = [];
1971
- let matchedDeepLinkValue = button.deepLinkValue || "";
1972
-
1973
- if (button.linkType === DEEP_LINK && button.deepLinkValue) {
1974
- // Parse deep links directly from accountData to avoid timing issues
1975
- let availableDeepLinks = [];
1976
- if (accountData?.configs?.deeplink) {
1977
- try {
1978
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
1979
- if (Array.isArray(parsedDeepLinks)) {
1980
- availableDeepLinks = parsedDeepLinks.map((link) => ({
1981
- label: link?.name,
1982
- value: link?.link,
1983
- title: link?.link,
1984
- keys: link?.keys,
1985
- }));
1986
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
1987
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
1988
- label: link?.name,
1989
- value: link?.link,
1990
- title: link?.link,
1991
- keys: link?.keys,
1992
- }));
1993
- }
1994
- } catch (error) {
1995
- console.error("[MobilePushNew] Error parsing deeplinks for library mode iOS carousel button:", error);
1996
- }
1997
- }
1998
-
1999
- if (availableDeepLinks.length > 0) {
2000
- // Remove query parameters from deepLinkValue to find the base deep link
2001
- let baseDeepLinkValue = button.deepLinkValue;
2002
- try {
2003
- const url = new URL(button.deepLinkValue);
2004
- baseDeepLinkValue = url.origin + url.pathname;
2005
- } catch (error) {
2006
- // If URL parsing fails, try to remove query parameters manually
2007
- const queryIndex = button.deepLinkValue.indexOf('?');
2008
- if (queryIndex !== -1) {
2009
- baseDeepLinkValue = button.deepLinkValue.substring(0, queryIndex);
2010
- }
2011
- }
2012
-
2013
- // Find matching deep link by comparing with the base deep link value
2014
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseDeepLinkValue);
2015
- if (matchingDeepLink) {
2016
- matchedDeepLinkValue = matchingDeepLink.value;
2017
- } else {
2018
- // If no exact match, try to find by partial match
2019
- const partialMatch = availableDeepLinks.find((link) =>
2020
- button.deepLinkValue.includes(link.value)
2021
- || link.value.includes(baseDeepLinkValue)
2022
- );
2023
- if (partialMatch) {
2024
- matchedDeepLinkValue = partialMatch.value;
2025
- }
2026
- }
2027
- }
2028
-
2029
- // Extract deep link keys from the original deepLinkValue URL
2030
- try {
2031
- const url = new URL(button.deepLinkValue);
2032
- const extractedKeys = [];
2033
- url.searchParams.forEach((value, key) => {
2034
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
2035
- extractedKeys.push(key);
2036
- }
2037
- });
2038
- if (extractedKeys.length > 0) {
2039
- deepLinkKeys = extractedKeys;
2040
- }
2041
- } catch (error) {
2042
- // If URL parsing fails, try manual extraction
2043
- const url = button.deepLinkValue;
2044
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
2045
- if (queryMatch) {
2046
- const extractedKeys = [];
2047
- queryMatch.forEach((param) => {
2048
- const [key, value] = param.replace(/^[?&]/, '').split('=');
2049
- if (key === value) { // Only extract keys where value equals key
2050
- extractedKeys.push(key);
2051
- }
2052
- });
2053
- if (extractedKeys.length > 0) {
2054
- deepLinkKeys = extractedKeys;
2055
- }
2056
- }
2057
- }
2058
- }
2059
-
2060
- return {
2061
- ...button,
2062
- deepLinkValue: matchedDeepLinkValue,
2063
- deepLinkKeys: deepLinkKeys,
2064
- };
2065
- }) : [],
2066
- }));
2067
-
2068
- // Initialize carousel images in Redux state for proper display in uploaders
2069
- if (carouselDataFromTemplate.length > 0) {
2070
- const firstCard = carouselDataFromTemplate[0];
2071
- if (firstCard.mediaType === 'image' && firstCard.imageUrl) {
2072
- // Set the first carousel image in Redux state for the uploader to recognize
2073
- setUpdateMpushImageSrc(firstCard.imageUrl, 1, CAROUSEL);
2074
- }
2075
- }
2076
- }
2077
- let iosButtonsValue = [];
2078
- if (Array.isArray(ctas)) {
2079
- iosButtonsValue = ctas;
2080
- }
2081
- const iosContentData = {
2082
- title: iosTitle,
2083
- message: iosMessage,
2084
- mediaType: iosMediaType,
2085
- buttons: iosButtonsValue,
2086
- actionOnClick: false,
2087
- linkType: DEEP_LINK,
2088
- deepLinkValue: "",
2089
- externalLinkValue: "",
2090
- imageSrc: iosImageSrc,
2091
- videoSrc: iosVideoSrc,
2092
- videoPreview: iosVideoPreview,
2093
- carouselData: carouselDataFromTemplate,
2094
- };
2095
- setIosContent(iosContentData);
2096
- if (iosImageSrc) {
2097
- setUpdateMpushImageSrc(iosImageSrc, 1, IMAGE);
2098
- }
2099
- if (iosVideoSrc) {
2100
- setUpdateMpushVideoSrc(1, {
2101
- videoSrc: iosVideoSrc,
2102
- previewUrl: iosVideoPreview,
2103
- }, true);
2104
- }
2105
- const iosButtons = iosExpandableDetails?.ctas || [];
2106
- if (iosButtons.length > 0) {
2107
- const ctaDataFromIos = iosButtons.map((button, index) => {
2108
- // Extract deep link keys from action link URL for button CTAs
2109
- let deepLinkKeys = [];
2110
- if (button.type === DEEP_LINK && button.actionLink) {
2111
- try {
2112
- const url = new URL(button.actionLink);
2113
- const extractedKeys = [];
2114
- url.searchParams.forEach((value, key) => {
2115
- if (value === key) { // Only extract keys where value equals key (deep link keys pattern)
2116
- extractedKeys.push(key);
2117
- }
2118
- });
2119
- if (extractedKeys.length > 0) {
2120
- deepLinkKeys = extractedKeys;
2121
- }
2122
- } catch (error) {
2123
- // If URL parsing fails, try manual extraction
2124
- const url = button.actionLink;
2125
- const queryMatch = url.match(/[?&]([^=&]+)=([^=&]+)/g);
2126
- if (queryMatch) {
2127
- const extractedKeys = [];
2128
- queryMatch.forEach((param) => {
2129
- const [key, value] = param.replace(/^[?&]/, '').split('=');
2130
- if (key === value) { // Only extract keys where value equals key
2131
- extractedKeys.push(key);
2132
- }
2133
- });
2134
- if (extractedKeys.length > 0) {
2135
- deepLinkKeys = extractedKeys;
2136
- }
2137
- }
2138
- }
2139
- }
2140
-
2141
- return {
2142
- text: button.actionText || button.label || "",
2143
- index,
2144
- url: (() => {
2145
- // For deep link buttons, find the matching deep link value
2146
- if (button.type === DEEP_LINK && button.actionLink) {
2147
- // Parse deep links directly from accountData to avoid timing issues
2148
- let availableDeepLinks = [];
2149
- if (accountData?.configs?.deeplink) {
2150
- try {
2151
- const parsedDeepLinks = JSON.parse(accountData.configs.deeplink || "[]");
2152
- if (Array.isArray(parsedDeepLinks)) {
2153
- availableDeepLinks = parsedDeepLinks.map((link) => ({
2154
- label: link?.name,
2155
- value: link?.link,
2156
- title: link?.link,
2157
- keys: link?.keys,
2158
- }));
2159
- } else if (typeof parsedDeepLinks === 'object' && parsedDeepLinks !== null) {
2160
- availableDeepLinks = Object.values(parsedDeepLinks).map((link) => ({
2161
- label: link?.name,
2162
- value: link?.link,
2163
- title: link?.link,
2164
- keys: link?.keys,
2165
- }));
2166
- }
2167
- } catch (error) {
2168
- console.error("[MobilePushNew] Error parsing deeplinks for iOS button CTA:", error);
2169
- }
2170
- }
2171
-
2172
- if (availableDeepLinks.length > 0) {
2173
- // Remove query parameters from actionLink to find the base deep link
2174
- let baseActionLink = button.actionLink;
2175
- try {
2176
- const url = new URL(button.actionLink);
2177
- baseActionLink = url.origin + url.pathname;
2178
- } catch (error) {
2179
- // If URL parsing fails, try to remove query parameters manually
2180
- const queryIndex = button.actionLink.indexOf('?');
2181
- if (queryIndex !== -1) {
2182
- baseActionLink = button.actionLink.substring(0, queryIndex);
2183
- }
2184
- }
2185
-
2186
- // Find matching deep link by comparing with the base action link
2187
- const matchingDeepLink = availableDeepLinks.find((link) => link.value === baseActionLink);
2188
- if (matchingDeepLink) {
2189
- return matchingDeepLink.value;
2190
- } else {
2191
- // If no exact match, try to find by partial match
2192
- const partialMatch = availableDeepLinks.find((link) =>
2193
- button.actionLink.includes(link.value)
2194
- || link.value.includes(baseActionLink)
2195
- );
2196
- if (partialMatch) {
2197
- return partialMatch.value;
2198
- }
2199
- }
2200
- }
2201
- }
2202
- return button.actionLink || "";
2203
- })(),
2204
- urlType: button.type || DEEP_LINK,
2205
- deepLinkKeys: deepLinkKeys,
2206
- ctaType: index === 0 ? PRIMARY : SECONDARY,
2207
- isSaved: true,
2208
- };
2209
- });
2210
- setCtaDataIos(ctaDataFromIos);
2211
- setPrimaryButtonIos(iosButtons.length >= 1);
2212
- setSecondaryButtonIos(iosButtons.length >= 2);
2213
- } else {
2214
- setCtaDataIos([]);
2215
- setPrimaryButtonIos(false);
2216
- setSecondaryButtonIos(false);
2217
- }
2218
- }
2219
- setSpin(false);
2220
- }, [isFullMode, templateData]);
2221
-
2222
- // Determine platform support from accountData
2223
- const isAndroidSupported = accountData?.configs?.android === '1';
2224
- const isIosSupported = accountData?.configs?.ios === '1';
2225
-
2226
- // Validation logic for template creation/update
2227
- const isAndroidFieldsMissing = isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim());
2228
- const isIosFieldsMissing = isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim());
2229
-
2230
- // Add changeSourceRef for debounced sync
2231
- const changeSourceRef = useRef(null);
2232
-
2233
- const {
2234
- handleTitleChange,
2235
- handleMessageChange,
2236
- handleTagSelect,
2237
- handleMediaTypeChange,
2238
- handleActionOnClickChange,
2239
- handleLinkTypeChange,
2240
- } = usePlatformSync({
2241
- setAndroidContent,
2242
- setIosContent,
2243
- validateTitle,
2244
- validateMessage,
2245
- setAndroidTitleError,
2246
- setAndroidMessageError,
2247
- setIosTitleError,
2248
- setIosMessageError,
2249
- androidContent,
2250
- iosContent,
2251
- updateOnMpushImageReUpload,
2252
- updateOnMpushVideoReUpload,
2253
- sameContent,
2254
- changeSourceRef,
2255
- });
2256
-
2257
- // Platform-specific deeplink and external link handlers
2258
- const handleDeepLinkChange = useCallback((platform, deepLinkValue) => {
2259
- // Validate deep link
2260
- const deepLinkError = validateDeepLink(deepLinkValue, formatMessage, messages);
2261
-
2262
- if (platform === ANDROID) {
2263
- setAndroidContent((prev) => {
2264
- const updated = {
2265
- ...prev,
2266
- deepLinkValue,
2267
- // Clear external link value when deep link is selected
2268
- externalLinkValue: "",
2269
- };
2270
- return updated;
2271
- });
2272
- setAndroidDeepLinkError(deepLinkError || "");
2273
- } else {
2274
- setIosContent((prev) => {
2275
- const updated = {
2276
- ...prev,
2277
- deepLinkValue,
2278
- // Clear external link value when deep link is selected
2279
- externalLinkValue: "",
2280
- };
2281
- return updated;
2282
- });
2283
- setIosDeepLinkError(deepLinkError || "");
2284
- }
2285
- }, [validateDeepLink]);
2286
-
2287
- const handleDeepLinkKeysChange = useCallback((platform, deepLinkKeysValue) => {
2288
- if (platform === ANDROID) {
2289
- setAndroidContent((prev) => {
2290
- const updated = {
2291
- ...prev,
2292
- deepLinkKeysValue,
2293
- };
2294
- return updated;
2295
- });
2296
- } else {
2297
- setIosContent((prev) => {
2298
- const updated = {
2299
- ...prev,
2300
- deepLinkKeysValue,
2301
- };
2302
- return updated;
2303
- });
2304
- }
2305
- }, []);
2306
-
2307
- const handleExternalLinkChange = useCallback((platform, externalLinkValue) => {
2308
- // Validate URL
2309
- const urlError = validateExternalLink(externalLinkValue, formatMessage, messages);
2310
-
2311
- if (platform === ANDROID) {
2312
- setAndroidContent((prev) => {
2313
- const updated = {
2314
- ...prev,
2315
- externalLinkValue,
2316
- // Clear deep link value when external link is entered
2317
- deepLinkValue: "",
2318
- };
2319
- return updated;
2320
- });
2321
- setAndroidExternalLinkError(urlError || "");
2322
- } else {
2323
- setIosContent((prev) => {
2324
- const updated = {
2325
- ...prev,
2326
- externalLinkValue,
2327
- // Clear deep link value when external link is entered
2328
- deepLinkValue: "",
2329
- };
2330
- return updated;
2331
- });
2332
- setIosExternalLinkError(urlError || "");
2333
- }
2334
- }, [validateExternalLink]);
2335
-
2336
- // Carousel data handler for platform-specific carousel management
2337
- const updateCarouselLinkError = useCallback((cardIndex, field, error) => {
2338
- setCarouselLinkErrors((prev) => ({
2339
- ...prev,
2340
- [`${cardIndex}-${field}`]: error,
2341
- }));
2342
- }, []);
2343
-
2344
- const handleCarouselDataChange = useCallback((platform, carouselData) => {
2345
- if (platform === ANDROID) {
2346
- setAndroidContent((prev) => {
2347
- const updated = { ...prev, carouselData };
2348
- return updated;
2349
- });
2350
- } else {
2351
- setIosContent((prev) => {
2352
- const updated = { ...prev, carouselData };
2353
- return updated;
2354
- });
2355
- }
2356
- }, []);
2357
-
2358
- // Update updateHandler to sync button state for both platforms if sameContent is true
2359
- const updateHandler = useCallback(
2360
- (ctaDataParam, index) => {
2361
- if (sameContent) {
2362
- setCtaDataAndroid((prevState) => {
2363
- const clonedCta = cloneDeep(prevState);
2364
- clonedCta[index] = ctaDataParam;
2365
- return clonedCta;
2366
- });
2367
- setCtaDataIos((prevState) => {
2368
- const clonedCta = cloneDeep(prevState);
2369
- clonedCta[index] = ctaDataParam;
2370
- return clonedCta;
2371
- });
2372
- // Update button state for both platforms
2373
- if (index === 0) {
2374
- setPrimaryButtonAndroid(true);
2375
- setPrimaryButtonIos(true);
2376
- } else if (index === 1) {
2377
- setSecondaryButtonAndroid(true);
2378
- setSecondaryButtonIos(true);
2379
- }
2380
- } else if (activeTab === ANDROID) {
2381
- setCtaDataAndroid((prevState) => {
2382
- const clonedCta = cloneDeep(prevState);
2383
- clonedCta[index] = ctaDataParam;
2384
- return clonedCta;
2385
- });
2386
- if (index === 0) setPrimaryButtonAndroid(true);
2387
- if (index === 1) setSecondaryButtonAndroid(true);
2388
- } else {
2389
- setCtaDataIos((prevState) => {
2390
- const clonedCta = cloneDeep(prevState);
2391
- clonedCta[index] = ctaDataParam;
2392
- return clonedCta;
2393
- });
2394
- if (index === 0) setPrimaryButtonIos(true);
2395
- if (index === 1) setSecondaryButtonIos(true);
2396
- }
2397
- },
2398
- [activeTab, sameContent]
2399
- );
2400
-
2401
- // Update deleteHandler to sync button state for both platforms if sameContent is true
2402
- const deleteHandler = useCallback((index) => {
2403
- if (sameContent) {
2404
- setCtaDataAndroid((prevState) => {
2405
- const clonedCta = cloneDeep(prevState);
2406
- const filteredCta = clonedCta.filter((cta) => cta.index !== index);
2407
- return filteredCta;
2408
- });
2409
- setCtaDataIos((prevState) => {
2410
- const clonedCta = cloneDeep(prevState);
2411
- const filteredCta = clonedCta.filter((cta) => cta.index !== index);
2412
- return filteredCta;
2413
- });
2414
- // Update button state for both platforms
2415
- if (index === 0) {
2416
- setPrimaryButtonAndroid(false);
2417
- setPrimaryButtonIos(false);
2418
- } else if (index === 1) {
2419
- setSecondaryButtonAndroid(false);
2420
- setSecondaryButtonIos(false);
2421
- }
2422
- } else if (activeTab === ANDROID) {
2423
- setCtaDataAndroid((prevState) => {
2424
- const clonedCta = cloneDeep(prevState);
2425
- const filteredCta = clonedCta.filter((cta) => cta.index !== index);
2426
- return filteredCta;
2427
- });
2428
- if (index === 0) setPrimaryButtonAndroid(false);
2429
- if (index === 1) setSecondaryButtonAndroid(false);
2430
- } else {
2431
- setCtaDataIos((prevState) => {
2432
- const clonedCta = cloneDeep(prevState);
2433
- const filteredCta = clonedCta.filter((cta) => cta.index !== index);
2434
- return filteredCta;
2435
- });
2436
- if (index === 0) setPrimaryButtonIos(false);
2437
- if (index === 1) setSecondaryButtonIos(false);
2438
- }
2439
- }, [activeTab, sameContent]);
2440
-
2441
- const onTagSelect = useCallback(
2442
- (data, id) => {
2443
- const platform = activeTab === ANDROID ? ANDROID : IOS;
2444
- handleTagSelect(platform, data, id === 0);
2445
- },
2446
- [activeTab, handleTagSelect]
2447
- );
2448
-
2449
- const renderContentFields = useCallback(
2450
- (platform) => {
2451
- const isAndroid = platform === ANDROID;
2452
- const currentContent = isAndroid ? androidContent : iosContent;
2453
- // Only show error if the platform is enabled
2454
- let titleError = "";
2455
- let messageError = "";
2456
- let externalLinkError = "";
2457
- let deepLinkError = "";
2458
- let deepLinkKeysError = "";
2459
- if (isAndroid) {
2460
- titleError = isAndroidSupported ? androidTitleError : "";
2461
- messageError = isAndroidSupported ? androidMessageError : "";
2462
- externalLinkError = isAndroidSupported ? androidExternalLinkError : "";
2463
- deepLinkError = isAndroidSupported ? androidDeepLinkError : "";
2464
- deepLinkKeysError = isAndroidSupported ? androidDeepLinkKeysError : "";
2465
- } else {
2466
- titleError = isIosSupported ? iosTitleError : "";
2467
- messageError = isIosSupported ? iosMessageError : "";
2468
- externalLinkError = isIosSupported ? iosExternalLinkError : "";
2469
- deepLinkError = isIosSupported ? iosDeepLinkError : "";
2470
- deepLinkKeysError = isIosSupported ? iosDeepLinkKeysError : "";
2471
- }
2472
- const primaryButton = isAndroid ? primaryButtonAndroid : primaryButtonIos;
2473
- const secondaryButton = isAndroid ? secondaryButtonAndroid : secondaryButtonIos;
2474
- const setPrimaryButton = isAndroid ? setPrimaryButtonAndroid : setPrimaryButtonIos;
2475
- const setSecondaryButton = isAndroid ? setSecondaryButtonAndroid : setSecondaryButtonIos;
2476
-
2477
- const handlers = {
2478
- handleTitleChange,
2479
- handleMessageChange,
2480
- handleMediaTypeChange,
2481
- handleActionOnClickChange,
2482
- handleLinkTypeChange,
2483
- handleDeepLinkChange: (value) => handleDeepLinkChange(platform, value),
2484
- handleDeepLinkKeysChange: (value) => handleDeepLinkKeysChange(platform, value),
2485
- handleExternalLinkChange: (value) => handleExternalLinkChange(platform, value),
2486
- onTagSelect,
2487
- handleOnTagsContextChange,
2488
- };
2489
-
2490
- const tagListProps = {
2491
- moduleFilterEnabled: location?.query?.type !== EMBEDDED,
2492
- label: formatMessage(messages.addLabels),
2493
- location,
2494
- tags: tags || [],
2495
- injectedTags: injectedTags || {},
2496
- selectedOfferDetails,
2497
- };
2498
-
2499
- // Fix nested ternary for videoAssetList and gifAssetList
2500
- let videoAssetList = {};
2501
- let gifAssetList = {};
2502
- if (currentContent?.mediaType === VIDEO) {
2503
- videoAssetList = platform === ANDROID ? androidAssetList : iosAssetList;
2504
- }
2505
- if (currentContent?.mediaType === GIF) {
2506
- gifAssetList = platform === ANDROID ? androidAssetList : iosAssetList;
2507
- }
2508
- const mediaUploaderProps = {
2509
- mediaType: currentContent?.mediaType,
2510
- activeTab: platform,
2511
- imageSrc,
2512
- uploadMpushAsset,
2513
- isFullMode,
2514
- setUpdateMpushImageSrc,
2515
- updateOnMpushImageReUpload,
2516
- imageData,
2517
- videoAssetList,
2518
- gifAssetList,
2519
- setUpdateMpushVideoSrc,
2520
- // Add video content state for fallback when Redux is empty (same pattern as imageSrc)
2521
- videoSrc: {
2522
- androidVideoSrc: videoState?.androidVideoSrc || '',
2523
- iosVideoSrc: videoState?.iosVideoSrc || '',
2524
- androidVideoPreview: videoState?.androidVideoPreview || '',
2525
- iosVideoPreview: videoState?.iosVideoPreview || '',
2526
- },
2527
- // Create truly platform-specific video data with no cross-platform information
2528
- videoDataForVideo: currentContent?.mediaType === VIDEO ? {
2529
- // Keep BOTH platforms available like images do - let MediaUploaders choose which to show
2530
- uploadedAssetData0: uploadedAssetData0 || {},
2531
- uploadedAssetData1: uploadedAssetData1 || {},
2532
- } : {},
2533
- videoDataForGif: currentContent?.mediaType === GIF ? {
2534
- // Keep BOTH platforms available like images do - let MediaUploaders choose which to show
2535
- uploadedAssetData0: uploadedAssetData0 || {},
2536
- uploadedAssetData1: uploadedAssetData1 || {},
2537
- } : {},
2538
- videoData,
2539
- clearImageDataByMediaType,
2540
- carouselData: currentContent?.carouselData || [],
2541
- onCarouselDataChange: handleCarouselDataChange,
2542
- mobilePushActions,
2543
- carouselActiveTabIndex,
2544
- setCarouselActiveTabIndex,
2545
- carouselLinkErrors,
2546
- updateCarouselLinkError,
2547
- linkProps: { deepLink },
2548
- videoPreviewKey: videoState?.videoPreviewKey,
2549
- };
2550
-
2551
- const ctaButtonProps = {
2552
- primaryButton,
2553
- secondaryButton,
2554
- setPrimaryButton,
2555
- setSecondaryButton,
2556
- ctaData,
2557
- updateHandler,
2558
- deleteHandler,
2559
- deepLink,
2560
- };
2561
-
2562
- const linkProps = {
2563
- deepLink,
2564
- deepLinkValue: currentContent?.deepLinkValue || "",
2565
- deepLinkKeysValue: currentContent?.deepLinkKeysValue || "",
2566
- externalLinkValue: currentContent?.externalLinkValue || "",
2567
- };
2568
-
2569
- return (
2570
- <PlatformContentFields
2571
- key={`platform-fields-${platform}`}
2572
- deviceType={platform}
2573
- content={currentContent}
2574
- errors={{
2575
- title: titleError,
2576
- message: messageError,
2577
- externalLink: externalLinkError,
2578
- deepLink: deepLinkError,
2579
- deepLinkKeys: deepLinkKeysError,
2580
- }}
2581
- handlers={handlers}
2582
- tagListProps={tagListProps}
2583
- mediaUploaderProps={mediaUploaderProps}
2584
- ctaButtonProps={ctaButtonProps}
2585
- linkProps={linkProps}
2586
- sameContent={sameContent}
2587
- formatMessage={formatMessage}
2588
- />
2589
- );
2590
- }, [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]
2591
- );
2592
-
2593
- // PANES: Only render enabled platforms
2594
- const PANES = useMemo(() => {
2595
- const panes = [];
2596
- if (isAndroidSupported) {
2597
- panes.push({
2598
- key: ANDROID,
2599
- tab: (
2600
- <CapRow>
2601
- <CapIcon type="android" />
2602
- {formatMessage(messages.android)}
2603
- </CapRow>
2604
- ),
2605
- content: renderContentFields(ANDROID),
2606
- });
2607
- }
2608
- if (isIosSupported) {
2609
- panes.push({
2610
- key: IOS,
2611
- tab: (
2612
- <CapRow>
2613
- <CapIcon type="ios" />
2614
- {formatMessage(messages.ios)}
2615
- </CapRow>
2616
- ),
2617
- content: renderContentFields(IOS),
2618
- });
2619
- }
2620
- return panes;
2621
- }, [isAndroidSupported, isIosSupported, renderContentFields, formatMessage]);
2622
-
2623
- // Save button disabled logic: only check enabled platforms
2624
- const isSaveDisabled = (
2625
- (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim()))
2626
- || (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim()))
2627
- || templateNameError
2628
- || Object.values(carouselLinkErrors).some((error) => error !== null && error !== "")
2629
- || !isCarouselDataValid()
2630
- );
2631
-
2632
- // Validation in handleSave: only show errors for enabled platforms
2633
- const handleSave = useCallback(() => {
2634
- if (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim())) {
2635
- CapNotification.error({
2636
- message: formatMessage(messages.androidValidationError),
2637
- });
2638
- if (onValidationFail) onValidationFail();
2639
- return;
2640
- }
2641
- if (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim())) {
2642
- CapNotification.error({
2643
- message: formatMessage(messages.iosValidationError),
2644
- });
2645
- if (onValidationFail) onValidationFail();
2646
- return;
2647
- }
2648
- if (templateNameError) {
2649
- CapNotification.error({
2650
- message: formatMessage(messages.emptyTemplateErrorMessage),
2651
- });
2652
- return;
2653
- }
2654
- // Only require templateName in full mode
2655
- if (isFullMode && !templateName?.trim()) {
2656
- CapNotification.error({
2657
- message: formatMessage(messages.emptyTemplateErrorMessage),
2658
- });
2659
- return;
2660
- }
2661
-
2662
- // In library mode, set templateName programmatically if empty
2663
- let finalTemplateName = templateName;
2664
- if (!isFullMode && (!finalTemplateName || !finalTemplateName.trim())) {
2665
- if (androidContent?.title && androidContent.title.trim()) {
2666
- finalTemplateName = androidContent.title.trim();
2667
- } else if (iosContent?.title && iosContent.title.trim()) {
2668
- finalTemplateName = iosContent.title.trim();
2669
- } else {
2670
- finalTemplateName = '';
2671
- }
2672
- }
2673
-
2674
- // Validate external links
2675
- const androidUrlError = validateExternalLink(androidContent.externalLinkValue, formatMessage, messages);
2676
- const iosUrlError = validateExternalLink(iosContent.externalLinkValue, formatMessage, messages);
2677
-
2678
- setAndroidExternalLinkError(androidUrlError || "");
2679
- setIosExternalLinkError(iosUrlError || "");
2680
-
2681
- // Validate deep links
2682
- const androidDeepLinkUrlError = validateDeepLink(androidContent.deepLinkValue, formatMessage, messages);
2683
- const iosDeepLinkUrlError = validateDeepLink(iosContent.deepLinkValue, formatMessage, messages);
2684
-
2685
- setAndroidDeepLinkError(androidDeepLinkUrlError || "");
2686
- setIosDeepLinkError(iosDeepLinkUrlError || "");
2687
-
2688
- // Validate deep link keys
2689
- const androidDeepLinkKeysErrorMsg = androidContent.linkType === DEEP_LINK && androidContent.deepLinkValue && (!Array.isArray(androidContent.deepLinkKeysValue) || androidContent.deepLinkKeysValue.length === 0) ? formatMessage(messages.deepLinkKeysRequired) : "";
2690
- const iosDeepLinkKeysErrorMsg = iosContent.linkType === DEEP_LINK && iosContent.deepLinkValue && (!Array.isArray(iosContent.deepLinkKeysValue) || iosContent.deepLinkKeysValue.length === 0) ? formatMessage(messages.deepLinkKeysRequired) : "";
2691
-
2692
- setAndroidDeepLinkKeysError(androidDeepLinkKeysErrorMsg || "");
2693
- setIosDeepLinkKeysError(iosDeepLinkKeysErrorMsg || "");
2694
-
2695
- // Check for carousel link errors
2696
- const hasCarouselErrors = Object.values(carouselLinkErrors).some((error) => error !== null && error !== "");
2697
-
2698
- if (androidUrlError || iosUrlError || androidDeepLinkUrlError || iosDeepLinkUrlError || androidDeepLinkKeysErrorMsg || iosDeepLinkKeysErrorMsg || hasCarouselErrors) {
2699
- CapNotification.error({
2700
- message: formatMessage(messages.invalidUrl),
2701
- });
2702
- if (onValidationFail) {
2703
- onValidationFail();
2704
- }
2705
- return;
2706
- }
2707
-
2708
- try {
2709
- // Convert ctaData to backend format and add to content
2710
- const processedAndroidContent = { ...androidContent };
2711
- const processedIosContent = { ...iosContent };
2712
-
2713
- // Process button CTA data if it exists
2714
- if (ctaDataAndroid && ctaDataAndroid.length > 0) {
2715
- const savedCtas = ctaDataAndroid.filter((cta) => cta.isSaved);
2716
- if (savedCtas.length > 0) {
2717
- processedAndroidContent.expandableDetails = {
2718
- ...processedAndroidContent.expandableDetails,
2719
- ctas: savedCtas.map((cta) => ({
2720
- actionText: cta.text,
2721
- type: cta.urlType || DEEP_LINK,
2722
- actionLink: cta.url,
2723
- deepLinkKeys: cta.deepLinkKeys, // Keep deepLinkKeys for createPayload.js to process
2724
- })),
2725
- };
2726
- }
2727
- }
2728
- if (ctaDataIos && ctaDataIos.length > 0) {
2729
- const savedCtas = ctaDataIos.filter((cta) => cta.isSaved);
2730
- if (savedCtas.length > 0) {
2731
- processedIosContent.expandableDetails = {
2732
- ...processedIosContent.expandableDetails,
2733
- ctas: savedCtas.map((cta) => ({
2734
- actionText: cta.text,
2735
- type: cta.urlType || DEEP_LINK,
2736
- actionLink: cta.url,
2737
- deepLinkKeys: cta.deepLinkKeys, // Keep deepLinkKeys for createPayload.js to process
2738
- })),
2739
- };
2740
- }
2741
- }
2742
-
2743
- // Fix: Only include enabled platform content in payload
2744
- const payload = createPayload({
2745
- templateName: finalTemplateName,
2746
- androidContent: isAndroidSupported ? processedAndroidContent : undefined,
2747
- iosContent: isIosSupported ? processedIosContent : undefined,
2748
- imageSrc,
2749
- mpushVideoSrcAndPreview,
2750
- accountData,
2751
- sameContent,
2752
- options: {
2753
- mode: params?.mode,
2754
- },
2755
- });
2756
-
2757
- // Add template ID for edit mode
2758
- if (isEditMode && templateId) {
2759
- payload._id = templateId;
2760
- }
2761
-
2762
- const content = getContent(payload);
2763
- const { definition: { mode: definitionMode } = {} } = payload || {};
2764
- const label = definitionMode === IMAGE.toLowerCase() ? IMAGE.toLowerCase() : TEXT.toLowerCase();
2765
-
2766
- const handleSuccess = (response) => {
2767
- const timeTaken = GA.timeTracker.stopTimer(TRACK_CREATE_MPUSH, {
2768
- category: "Creatives",
2769
- action: TRACK_CREATE_MPUSH,
2770
- label,
2771
- });
2772
-
2773
- gtmPush("creativeDetails", {
2774
- id: response?.templateId || params?.id,
2775
- name: payload.name,
2776
- channel: MOBILE_PUSH_CHANNEL,
2777
- timeTaken,
2778
- content,
2779
- mode: isEditMode ? EDIT : CREATE,
2780
- imageAdded: definitionMode === IMAGE.toLowerCase(),
2781
- });
2782
-
2783
- // --- BEGIN: Library mode communication fix ---
2784
- if (!isFullMode) {
2785
- // In library mode, only communicate to parent/callback, do NOT call create/edit API
2786
- const formDataForLibrary = {
2787
- action: 'getFormData',
2788
- value: payload, // payload is the backend-ready template data with versions.base structure
2789
- validity: true,
2790
- type: 'MOBILEPUSH',
2791
- };
2792
- if (window && window.parent) {
2793
- window.parent.postMessage(JSON.stringify(formDataForLibrary), '*');
2794
- }
2795
- if (typeof getFormLibraryData === 'function') {
2796
- try {
2797
- getFormLibraryData(formDataForLibrary);
2798
- } catch (error) {
2799
- console.error('[MobilePushNew] Error calling getFormLibraryData:', error);
2800
- }
2801
- }
2802
-
2803
- // Close slidebox after successful save in library mode
2804
- if (typeof handleClose === 'function') {
2805
- handleClose();
2806
- }
2807
-
2808
- // Do not proceed with any API or further side effects in library mode
2809
- return;
2810
- }
2811
- // --- END: Library mode communication fix ---
2812
-
2813
- // Show success toast and refresh list after create (not edit)
2814
- if (!isEditMode && response && (response.templateId || response._id)) {
2815
- const message = `${intl.formatMessage(messages.templateCreateSuccess)}`;
2816
- CapNotification.success({ key: 'createSuccess', message });
2817
- // Call handleClose to close the slidebox/modal
2818
- if (typeof handleClose === 'function') {
2819
- handleClose();
2820
- }
2821
- // Call onCreateComplete to notify parent to refresh list
2822
- if (typeof onCreateComplete === 'function') {
2823
- onCreateComplete(true);
2824
- }
2825
- // Optionally, trigger a parent refresh (if needed)
2826
- if (window && window.parent) {
2827
- window.parent.postMessage(JSON.stringify({ type: 'REFRESH_MOBILEPUSH_TEMPLATES' }), '*');
2828
- }
2829
- setTimeout(() => {
2830
- mobilePushActions.clearCreateResponse();
2831
- }, 100);
2832
- }
2833
-
2834
- if (isEditMode) {
2835
- // Delay handleClose to ensure Templates container processes the response first
2836
- setTimeout(() => {
2837
- try {
2838
- handleClose();
2839
- } catch (error) {
2840
- console.error("[MobilePushNew] Error calling handleClose:", error);
2841
- }
2842
- }, 150);
2843
- // Delay clearing the response to allow Templates container to process it
2844
- setTimeout(() => {
2845
- mobilePushActions.clearEditResponse();
2846
- }, 100);
2847
- } else {
2848
- resetFormData();
2849
- }
2850
-
2851
- if (getFormLibraryData && isFullMode) {
2852
- getFormLibraryData({ validity: true });
2853
- }
2854
- };
2855
-
2856
- // --- Only call create/edit API in full mode ---
2857
- if (isFullMode) {
2858
- if (isEditMode) {
2859
- mobilePushActions.editTemplate(payload, (response) => {
2860
- // Guard: If response is an Error object, show error notification and return
2861
- if (response instanceof Error) {
2862
- CapNotification.error({ key: 'createError', message: intl.formatMessage(messages.somethingWentWrong) });
2863
- return;
2864
- }
2865
- handleSuccess({ ...response, isEdit: true });
2866
- });
2867
- } else {
2868
- mobilePushActions.createTemplate(payload, (response) => {
2869
- // Guard: If response is an Error object, show error notification and return
2870
- if (response instanceof Error) {
2871
- const errorMsg = response.message || response.toString();
2872
- // Check for duplicate name error
2873
- if (errorMsg && errorMsg.toLowerCase().includes('template name already exist')) {
2874
- CapNotification.error({ key: 'duplicateName', message: errorMsg });
2875
- // Do NOT close the slidebox or call onCreateComplete
2876
- return;
2877
- }
2878
- CapNotification.error({ key: 'createError', message: intl.formatMessage(messages.somethingWentWrong) });
2879
- if (typeof onCreateComplete === 'function') {
2880
- onCreateComplete(false);
2881
- }
2882
- return;
2883
- }
2884
- handleSuccess({ ...response, isEdit: false });
2885
- });
2886
- }
2887
- } else {
2888
- // In library mode, do NOT call create/edit API, just communicate to parent/callback (already handled above)
2889
- // Call handleSuccess directly to trigger the postMessage logic
2890
- handleSuccess({ isLibraryMode: true });
2891
- }
2892
- } catch (error) {
2893
- const errorMsg = getMessageObject(
2894
- "error",
2895
- formatMessage(messages.somethingWentWrong),
2896
- true
2897
- );
2898
- globalActionsProps.addMessageToQueue(errorMsg);
2899
- if (onValidationFail) {
2900
- onValidationFail();
2901
- }
2902
- }
2903
- }, [
2904
- templateNameError,
2905
- formatMessage,
2906
- templateName,
2907
- androidContent,
2908
- iosContent,
2909
- imageSrc,
2910
- mpushVideoSrcAndPreview,
2911
- accountData,
2912
- params?.mode,
2913
- params?.id,
2914
- isEditMode,
2915
- mobilePushActions,
2916
- handleClose,
2917
- getFormLibraryData,
2918
- globalActionsProps,
2919
- onValidationFail,
2920
- resetFormData,
2921
- validateExternalLink,
2922
- ctaDataAndroid,
2923
- ctaDataIos,
2924
- intl,
2925
- onCreateComplete,
2926
- createTimeoutRef,
2927
- isAndroidSupported,
2928
- isIosSupported,
2929
- isAndroidFieldsMissing,
2930
- isIosFieldsMissing,
2931
- isCarouselDataValid,
2932
- isFullMode,
2933
- templateId,
2934
- ]);
2935
-
2936
- // Helper to sync content between platforms
2937
- const syncPlatformContent = useCallback((sourceContent, sourcePlatform) => {
2938
- const targetPlatform = sourcePlatform === ANDROID ? IOS : ANDROID;
2939
- const setTargetContent = sourcePlatform === ANDROID ? setIosContent : setAndroidContent;
2940
- const setTargetTitleError = sourcePlatform === ANDROID ? setIosTitleError : setAndroidTitleError;
2941
- const setTargetMessageError = sourcePlatform === ANDROID ? setIosMessageError : setAndroidMessageError;
2942
- // Sync all content fields
2943
- setTargetContent({
2944
- ...sourceContent,
2945
- // Preserve platform-specific fields if any
2946
- buttons: Array.isArray(sourceContent.buttons)
2947
- ? sourceContent.buttons.map((btn) => ({
2948
- ...btn,
2949
- platform: targetPlatform,
2950
- }))
2951
- : [],
2952
- });
2953
-
2954
- // Validate synced content
2955
- setTargetTitleError(validateTitle(sourceContent.title));
2956
- setTargetMessageError(validateMessage(sourceContent.message));
2957
- }, []);
2958
-
2959
- const onTemplateNameChange = useCallback(
2960
- (ev) => {
2961
- const { value } = ev.target;
2962
- setTemplateName(value);
2963
- const isInvalid = !value || value.trim() === "";
2964
- setTemplateNameError(isInvalid);
2965
- if (value && onEnterTemplateName) {
2966
- onEnterTemplateName();
2967
- } else if (onRemoveTemplateName) {
2968
- onRemoveTemplateName();
2969
- }
2970
- },
2971
- [onEnterTemplateName, onRemoveTemplateName]
2972
- );
2973
-
2974
- // --- Only show template name input in full mode (not library/consumer mode) ---
2975
- const createModeContent = useCallback(
2976
- () => (
2977
- isFullMode ? (
2978
- <CapRow className="input-group creative-name-container">
2979
- <CapInput
2980
- id="mobile-push-template-name-input"
2981
- className="mobile-push-template-name-input"
2982
- key={`template-name-${params?.id || 'create'}`}
2983
- label={formatMessage(messages.creativeName)}
2984
- placeholder={formatMessage(messages.creativeNamePlaceholder)}
2985
- value={templateName}
2986
- onChange={onTemplateNameChange}
2987
- status={templateNameError ? "error" : ""}
2988
- help={
2989
- templateNameError
2990
- ? formatMessage(messages.emptyTemplateErrorMessage)
2991
- : ""
2992
- }
2993
- />
2994
- </CapRow>
2995
- ) : null
2996
- ),
2997
- [isFullMode, formatMessage, templateName, onTemplateNameChange, templateNameError, params?.id]
2998
- );
2999
-
3000
- const liquidMiddleWare = useCallback(() => {
3001
- const onError = ({ standardErrors, liquidErrors }) => {
3002
- setErrorMessage((prev) => ({
3003
- STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
3004
- LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
3005
- }));
3006
- };
3007
- const onSuccess = () => handleSave();
3008
-
3009
- validateMobilePushContent([androidContent, iosContent], {
3010
- currentTab: activeTab === ANDROID ? 1 : 2,
3011
- onError,
3012
- onSuccess,
3013
- getLiquidTags: globalActionsProps.getLiquidTags,
3014
- formatMessage,
3015
- messages: formBuilderMessages,
3016
- tagLookupMap: metaEntities?.tagLookupMap || {},
3017
- eventContextTags: metaEntities?.eventContextTags || [],
3018
- isLiquidFlow: hasLiquidSupportFeature(),
3019
- forwardedTags: {},
3020
- skipTags: (tag) => {
3021
- const skipRegexes = [
3022
- /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
3023
- /unsubscribe\(#[a-zA-Z\d]{6}\)/,
3024
- /Link_to_[a-zA-z]/,
3025
- /SURVEY.*\.TOKEN/,
3026
- /^[A-Za-z].*\([a-zA-Z\d]*\)/,
3027
- ];
3028
- return skipRegexes.some((regex) => regex.test(tag));
3029
- },
3030
- singleTab: getSingleTab(accountData),
3031
- });
3032
- }, [
3033
- androidContent,
3034
- iosContent,
3035
- activeTab,
3036
- globalActionsProps,
3037
- formatMessage,
3038
- metaEntities,
3039
- accountData,
3040
- ]);
3041
-
3042
- const isLiquidFlow = hasLiquidSupportFeature();
3043
-
3044
- useEffect(() => {
3045
- // Always map to { label } for both platforms
3046
- const newButtons = Array.isArray(ctaData)
3047
- ? ctaData.map((data) => ({
3048
- label: data?.text || '',
3049
- }))
3050
- : [];
3051
-
3052
- if (activeTab === ANDROID) {
3053
- setAndroidContent((prevContent) => ({
3054
- ...prevContent,
3055
- buttons: newButtons,
3056
- }));
3057
- } else if (activeTab === IOS) {
3058
- setIosContent((prevContent) => ({
3059
- ...prevContent,
3060
- buttons: newButtons,
3061
- }));
3062
- }
3063
- }, [ctaData, activeTab]);
3064
-
3065
- // Sync button data and state for both platforms when sameContent is enabled (e.g., in edit mode)
3066
- useEffect(() => {
3067
- if (sameContent) {
3068
- // Copy button data and state from the active tab to the other platform
3069
- if (activeTab === ANDROID) {
3070
- setCtaDataIos(ctaDataAndroid);
3071
- setPrimaryButtonIos(primaryButtonAndroid);
3072
- setSecondaryButtonIos(secondaryButtonAndroid);
3073
- } else {
3074
- setCtaDataAndroid(ctaDataIos);
3075
- setPrimaryButtonAndroid(primaryButtonIos);
3076
- setSecondaryButtonAndroid(secondaryButtonIos);
3077
- }
3078
- }
3079
- // Only run when sameContent or activeTab changes
3080
- }, [sameContent]);
3081
-
3082
- const handleSameContentChange = useCallback((e) => {
3083
- const isCurrentPlatformAndroid = activeTab === ANDROID;
3084
- syncPlatformContent(isCurrentPlatformAndroid ? androidContent : iosContent, isCurrentPlatformAndroid ? ANDROID : IOS);
3085
- setSameContent(e.target.checked);
3086
- }, [activeTab, androidContent, iosContent]);
3087
-
3088
- const getContentType = useCallback(
3089
- (content) => {
3090
- // Get image source from multiple possible locations
3091
- const getImageSource = () => {
3092
- // First check content-specific image - handle both string and object formats
3093
- if (content?.imageSrc) {
3094
- // If imageSrc is a string, return it directly
3095
- if (typeof content.imageSrc === 'string') {
3096
- return content.imageSrc;
3097
- }
3098
- // If imageSrc is an object, extract the URL based on active tab
3099
- if (typeof content.imageSrc === 'object') {
3100
- if (activeTab === ANDROID && content.imageSrc.androidImageSrc) {
3101
- return content.imageSrc.androidImageSrc;
3102
- }
3103
- if (activeTab === IOS && content.imageSrc.iosImageSrc) {
3104
- return content.imageSrc.iosImageSrc;
3105
- }
3106
- return '';
3107
- }
3108
- }
3109
-
3110
- // Then check upload state based on active tab
3111
- if (activeTab === ANDROID && imageSrc?.androidImageSrc) {
3112
- return imageSrc.androidImageSrc;
3113
- }
3114
- if (activeTab === IOS && imageSrc?.iosImageSrc) {
3115
- return imageSrc.iosImageSrc;
3116
- }
3117
-
3118
- return '';
3119
- };
3120
-
3121
- // Get video source from multiple possible locations
3122
- const getVideoSource = () => {
3123
- // First check current platform's content
3124
- if (content?.videoSrc) {
3125
- return content.videoSrc;
3126
- }
3127
-
3128
- // Then check current platform's upload state only - DEFENSIVE: ensure perfect symmetry
3129
- const uploadVideoSrc = activeTab === ANDROID
3130
- ? videoState?.androidVideoSrc || ''
3131
- : videoState?.iosVideoSrc || '';
3132
- return uploadVideoSrc;
3133
- };
3134
-
3135
- const getVideoPreview = () => {
3136
- // First check current platform's content
3137
- if (content?.videoPreview) {
3138
- return content.videoPreview;
3139
- }
3140
-
3141
- // Then check current platform's upload state only - DEFENSIVE: ensure perfect symmetry
3142
- const uploadVideoPreview = activeTab === ANDROID
3143
- ? videoState?.androidVideoPreview || ''
3144
- : videoState?.iosVideoPreview || '';
3145
- return uploadVideoPreview;
3146
- };
3147
-
3148
- const videoSrc = getVideoSource();
3149
- const videoPreview = getVideoPreview();
3150
-
3151
- const bodyGifValue = content?.mediaType === GIF ? (videoSrc || getImageSource()) : "";
3152
-
3153
- const previewData = {
3154
- appName: editData?.selectedWeChatAccount?.name || accountData?.selectedWeChatAccount?.name || "",
3155
- bodyText: content?.message || "",
3156
- bodyImage: content?.mediaType === IMAGE ? getImageSource() : "",
3157
- actions: Array.isArray(content?.buttons) ? content.buttons : [],
3158
- carouselData: content?.carouselData || [],
3159
- header: content?.title || "",
3160
- bodyVideo: (content?.mediaType === VIDEO || content?.mediaType === GIF) ? {
3161
- videoSrc,
3162
- videoPreview,
3163
- } : {},
3164
- bodyGif: bodyGifValue,
3165
- };
3166
- return previewData;
3167
- },
3168
- [editData, accountData, imageSrc, mpushVideoSrcAndPreview, activeTab, videoState]
3169
- );
3170
-
3171
- const previewContent = useMemo(() => {
3172
- const currentContent = activeTab === ANDROID ? androidContent : iosContent;
3173
- const preview = getContentType(currentContent);
3174
- return preview;
3175
- }, [activeTab, androidContent, iosContent, getContentType]);
3176
-
3177
- // Add useEffect to handle isGetFormData prop changes
3178
- useEffect(() => {
3179
- if (isGetFormData) {
3180
- handleSave();
3181
- // Reset the flag to prevent infinite loop
3182
- if (onValidationFail) {
3183
- onValidationFail();
3184
- }
3185
- }
3186
- }, [isGetFormData, handleSave, onValidationFail]);
3187
-
3188
- // Add message event listener to handle parent communication (like old MobilePush components)
3189
- useEffect(() => {
3190
- const handleFrameTasks = (e) => {
3191
- const { data: type } = e;
3192
-
3193
- if (typeof type === 'object') {
3194
- const { action } = type;
3195
- switch (action) {
3196
- case "getFormData":
3197
- handleSave();
3198
- break;
3199
- case "startTemplateCreation":
3200
- // Handle template creation start if needed
3201
- break;
3202
- default:
3203
- break;
3204
- }
3205
- } else {
3206
- switch (type) {
3207
- case "getFormData":
3208
- handleSave();
3209
- break;
3210
- default:
3211
- break;
3212
- }
3213
- }
3214
- };
3215
-
3216
- window.addEventListener("message", handleFrameTasks);
3217
-
3218
- return () => {
3219
- window.removeEventListener("message", handleFrameTasks);
3220
- };
3221
- }, [handleSave]);
3222
-
3223
- return (
3224
- <CapSpin
3225
- spinning={spin || editData?.editTemplateInProgress || fetchingLiquidValidation || getTemplateDetailsInProgress}
3226
- tip={
3227
- fetchingLiquidValidation && formatMessage(messages.validationLoadingMessage)
3228
- }
3229
- >
3230
- <CapRow className="mobile-push-container">
3231
- <CapColumn className="content-section" span={14}>
3232
- {createModeContent()}
3233
- {isAndroidSupported && isIosSupported && (
3234
- <CapCheckbox
3235
- className="same-content-checkbox"
3236
- checked={sameContent}
3237
- onChange={handleSameContentChange}
3238
- key={`same-content-${sameContent}`}
3239
- >
3240
- {formatMessage(messages.sameContentForBothChannels)}
3241
- </CapCheckbox>
3242
- )}
3243
- <CapRow className="platform-header">
3244
- <CapTab
3245
- className="platform-tabs"
3246
- activeKey={activeTab}
3247
- onChange={setActiveTab}
3248
- panes={PANES}
3249
- />
3250
- </CapRow>
3251
- <CapRow>
3252
- <CapColumn span={24}>
3253
- <ErrorInfoNote
3254
- currentTab={activeTab?.toUpperCase()}
3255
- errorMessages={{
3256
- LIQUID_ERROR_MSG: errorMessage?.LIQUID_ERROR_MSG,
3257
- STANDARD_ERROR_MSG: errorMessage?.STANDARD_ERROR_MSG,
3258
- }}
3259
- />
3260
-
3261
- {/* In library mode, only show save button in create mode (edit mode has Done button from FormBuilder). In full mode, keep old logic. */}
3262
- {((!isFullMode && computedCreativesMode !== 'edit') || (isFullMode && !isEditMode && computedCreativesMode === 'create')) && (
3263
- <CapRow className="save-section">
3264
- <CapColumn span={24}>
3265
- <CapButton
3266
- type="primary"
3267
- onClick={() => {
3268
- if (isLiquidFlow) {
3269
- liquidMiddleWare();
3270
- } else {
3271
- handleSave();
3272
- }
3273
- }}
3274
- className="save-button"
3275
- disabled={isSaveDisabled}
3276
- >
3277
- {formatMessage(messages.saveTemplate)}
3278
- </CapButton>
3279
- </CapColumn>
3280
- </CapRow>
3281
- )}
3282
-
3283
- </CapColumn>
3284
- </CapRow>
3285
- </CapColumn>
3286
- <CapColumn className="preview-section" span={10}>
3287
- <TemplatePreview
3288
- device={activeTab === ANDROID ? "android" : "iphone"}
3289
- content={previewContent}
3290
- channel={MOBILE_PUSH_CHANNEL}
3291
- templateData={templateData}
3292
- />
3293
- </CapColumn>
3294
- </CapRow>
3295
- </CapSpin>
3296
- );
3297
- };
3298
-
3299
- MobilePushNew.propTypes = {
3300
- isFullMode: PropTypes.bool,
3301
- onEnterTemplateName: PropTypes.func,
3302
- onRemoveTemplateName: PropTypes.func,
3303
- intl: intlShape.isRequired,
3304
- location: PropTypes.object,
3305
- selectedOfferDetails: PropTypes.object,
3306
- getDefaultTags: PropTypes.func,
3307
- injectedTags: PropTypes.object,
3308
- params: PropTypes.object,
3309
- templateData: PropTypes.object,
3310
- accountData: PropTypes.object,
3311
- editData: PropTypes.object,
3312
- mobilePushActions: PropTypes.object,
3313
- onValidationFail: PropTypes.func,
3314
- getFormLibraryData: PropTypes.func,
3315
- uploadedAssetData: PropTypes.object,
3316
- uploadedAssetData0: PropTypes.object,
3317
- uploadedAssetData1: PropTypes.object,
3318
- uploadAssetSuccess: PropTypes.bool,
3319
- metaEntities: PropTypes.object,
3320
- supportedTags: PropTypes.array,
3321
- globalActions: PropTypes.object,
3322
- fetchingLiquidValidation: PropTypes.bool,
3323
- handleClose: PropTypes.func,
3324
- createTemplateError: PropTypes.any,
3325
- isGetFormData: PropTypes.bool,
3326
- getTemplateDetailsInProgress: PropTypes.bool,
3327
- onCreateComplete: PropTypes.func,
3328
- };
3329
-
3330
- MobilePushNew.defaultProps = {
3331
- isFullMode: false,
3332
- onEnterTemplateName: () => {},
3333
- onRemoveTemplateName: () => {},
3334
- location: {},
3335
- selectedOfferDetails: {},
3336
- getDefaultTags: () => {},
3337
- injectedTags: {},
3338
- params: {},
3339
- templateData: {},
3340
- accountData: {},
3341
- editData: {},
3342
- mobilePushActions: {},
3343
- onValidationFail: () => {},
3344
- getFormLibraryData: () => {},
3345
- uploadedAssetData: {},
3346
- uploadedAssetData0: {},
3347
- uploadedAssetData1: {},
3348
- uploadAssetSuccess: false,
3349
- metaEntities: {},
3350
- supportedTags: [],
3351
- globalActions: {},
3352
- fetchingLiquidValidation: false,
3353
- handleClose: () => {},
3354
- createTemplateError: null,
3355
- isGetFormData: false,
3356
- getTemplateDetailsInProgress: false,
3357
- onCreateComplete: () => {},
3358
- };
3359
-
3360
- const mapStateToProps = createStructuredSelector({
3361
- editData: makeSelectMobilePushNew(),
3362
- injectedTags: setInjectedTags(),
3363
- currentOrgDetails: selectCurrentOrgDetails(),
3364
- metaEntities: makeSelectMetaEntities(),
3365
- loadingTags: isLoadingMetaEntities(),
3366
- uploadedAssetData: makeSelectUploadedAssetData(),
3367
- uploadedAssetData0: makeSelectUploadedAssetData0(),
3368
- uploadedAssetData1: makeSelectUploadedAssetData1(),
3369
- uploadAssetSuccess: makeSelectUploadAssetSuccess(),
3370
- assetUploading: makeSelectAssetUploading(),
3371
- fetchingLiquidValidation: selectLiquidStateDetails(),
3372
- createTemplateError: makeSelectCreateError(),
3373
- supportedTags: () => [],
3374
- accountData: createSelector(
3375
- (state) => state.get('templates'),
3376
- (templatesState) => {
3377
- if (!templatesState) {
3378
- return {};
3379
- }
3380
- const templates = templatesState.toJS();
3381
- const selectedAccount = templates?.selectedWeChatAccount || {};
3382
- // Debug logging removed - issue identified and fixed
3383
- return selectedAccount;
3384
- }
3385
- ),
3386
- getTemplateDetailsInProgress: makeSelectGetTemplateDetailsInProgress(),
3387
- });
3388
-
3389
- const mapDispatchToProps = (dispatch) => ({
3390
- mobilePushActions: bindActionCreators(actions, dispatch),
3391
- globalActions: bindActionCreators(globalActions, dispatch),
3392
- });
3393
-
3394
- const withSaga = injectSaga({
3395
- key: "mobilePushNew",
3396
- saga: v2MobilePushSagas,
3397
- mode: DAEMON,
3398
- });
3399
-
3400
- const withReducer = injectReducer({
3401
- key: "mobilePushNew",
3402
- reducer: mobilePushReducer,
3403
- });
3404
-
3405
- export default withCreatives({
3406
- WrappedComponent: MobilePushNew,
3407
- mapStateToProps,
3408
- mapDispatchToProps,
3409
- userAuth: true,
3410
- sagas: [withSaga],
3411
- reducers: [withReducer],
3412
- });