@capillarytech/creatives-library 8.0.125-alpha.6 → 8.0.126

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