@capillarytech/creatives-library 8.0.123 → 8.0.124

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