@capillarytech/creatives-library 8.0.259 → 8.0.260-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/constants/unified.js +1 -2
- package/initialReducer.js +0 -2
- package/package.json +1 -1
- package/services/api.js +0 -10
- package/services/tests/api.test.js +0 -34
- package/translations/en.json +3 -4
- package/utils/common.js +0 -12
- package/utils/commonUtils.js +5 -28
- package/utils/tests/commonUtil.test.js +0 -224
- package/utils/transformTemplateConfig.js +10 -0
- package/v2Components/CapDeviceContent/index.js +56 -61
- package/v2Components/CapTagList/index.js +1 -6
- package/v2Components/CapTagListWithInput/index.js +1 -5
- package/v2Components/CapTagListWithInput/messages.js +1 -1
- package/v2Components/CapWhatsappCTA/tests/index.test.js +0 -5
- package/v2Components/ErrorInfoNote/index.js +72 -457
- package/v2Components/ErrorInfoNote/messages.js +6 -36
- package/v2Components/ErrorInfoNote/style.scss +6 -282
- package/v2Components/FormBuilder/index.js +4 -4
- package/v2Components/FormBuilder/tests/index.test.js +4 -13
- package/v2Components/HtmlEditor/HTMLEditor.js +94 -547
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +133 -1441
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +16 -27
- package/v2Components/HtmlEditor/_htmlEditor.scss +45 -108
- package/v2Components/HtmlEditor/_index.lazy.scss +1 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +102 -23
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +140 -148
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +1 -2
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +0 -9
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +4 -4
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +0 -22
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +7 -4
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +45 -35
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +3 -1
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +6 -7
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +6 -3
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +43 -22
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +0 -1
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +31 -49
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +34 -50
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +41 -70
- package/v2Components/HtmlEditor/constants.js +20 -42
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +16 -373
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +16 -120
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +2 -5
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +146 -88
- package/v2Components/HtmlEditor/hooks/useValidation.js +53 -189
- package/v2Components/HtmlEditor/index.js +1 -1
- package/v2Components/HtmlEditor/messages.js +94 -92
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +45 -94
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +0 -134
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +41 -40
- package/v2Components/HtmlEditor/utils/htmlValidator.js +72 -71
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +102 -134
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +25 -23
- package/v2Components/HtmlEditor/utils/validationAdapter.js +41 -66
- package/v2Components/MobilePushPreviewV2/index.js +7 -32
- package/v2Components/TemplatePreview/_templatePreview.scss +24 -55
- package/v2Components/TemplatePreview/index.js +32 -47
- package/v2Components/TemplatePreview/messages.js +0 -4
- package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +0 -1
- package/v2Containers/BeeEditor/index.js +90 -172
- package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +3 -4
- package/v2Containers/CreativesContainer/SlideBoxContent.js +52 -128
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -163
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +1 -2
- package/v2Containers/CreativesContainer/constants.js +0 -1
- package/v2Containers/CreativesContainer/index.js +46 -240
- package/v2Containers/CreativesContainer/messages.js +0 -8
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +2 -11
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +50 -38
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +0 -106
- package/v2Containers/Email/actions.js +0 -7
- package/v2Containers/Email/constants.js +1 -5
- package/v2Containers/Email/index.js +30 -239
- package/v2Containers/Email/messages.js +0 -32
- package/v2Containers/Email/reducer.js +1 -12
- package/v2Containers/Email/sagas.js +7 -61
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +0 -2
- package/v2Containers/Email/tests/reducer.test.js +0 -46
- package/v2Containers/Email/tests/sagas.test.js +29 -320
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +21 -211
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +74 -40
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +67 -2
- package/v2Containers/EmailWrapper/constants.js +0 -2
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +77 -629
- package/v2Containers/EmailWrapper/index.js +23 -103
- package/v2Containers/EmailWrapper/messages.js +1 -65
- package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +214 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +77 -594
- package/v2Containers/InApp/actions.js +0 -7
- package/v2Containers/InApp/constants.js +4 -20
- package/v2Containers/InApp/index.js +360 -804
- package/v2Containers/InApp/index.scss +3 -4
- package/v2Containers/InApp/messages.js +3 -7
- package/v2Containers/InApp/reducer.js +3 -21
- package/v2Containers/InApp/sagas.js +9 -29
- package/v2Containers/InApp/selectors.js +5 -25
- package/v2Containers/InApp/tests/index.test.js +71 -152
- package/v2Containers/InApp/tests/reducer.test.js +0 -34
- package/v2Containers/InApp/tests/sagas.test.js +9 -61
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +12 -39
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +6 -10
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +75 -102
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +54 -81
- package/v2Containers/MobilePushNew/index.js +2 -3
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +178 -262
- package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +12 -16
- package/v2Containers/SmsTrai/Edit/index.js +1 -2
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +111 -468
- package/v2Containers/TagList/index.js +19 -62
- package/v2Containers/Templates/_templates.scss +1 -60
- package/v2Containers/Templates/index.js +4 -89
- package/v2Containers/Templates/messages.js +0 -4
- package/v2Containers/WebPush/Create/messages.js +8 -0
- package/v2Containers/WebPush/Create/preview/PreviewControls.js +2 -2
- package/v2Containers/WebPush/Create/preview/PreviewDisclaimer.js +3 -1
- package/v2Containers/WebPush/Create/preview/components/AndroidMobileChromeHeader.js +5 -1
- package/v2Containers/WebPush/Create/preview/components/AndroidMobileExpanded.js +5 -1
- package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/AndroidMobileExpanded.test.js.snap +5 -1
- package/v2Containers/WebPush/Create/preview/preview.scss +7 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +734 -1306
- package/v2Components/ErrorInfoNote/constants.js +0 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -874
- package/v2Components/HtmlEditor/components/ValidationPanel/constants.js +0 -6
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +0 -255
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +0 -364
- package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +0 -51
- package/v2Components/HtmlEditor/utils/validationConstants.js +0 -40
- package/v2Containers/BeePopupEditor/_beePopupEditor.scss +0 -14
- package/v2Containers/BeePopupEditor/constants.js +0 -10
- package/v2Containers/BeePopupEditor/index.js +0 -194
- package/v2Containers/BeePopupEditor/tests/index.test.js +0 -627
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +0 -1285
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +0 -1880
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +0 -520
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +0 -643
- package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +0 -376
- package/v2Containers/InApp/__tests__/sagas.test.js +0 -363
- package/v2Containers/InApp/tests/selectors.test.js +0 -612
- package/v2Containers/InAppWrapper/components/InAppWrapperView.js +0 -151
- package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +0 -267
- package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +0 -23
- package/v2Containers/InAppWrapper/constants.js +0 -16
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +0 -473
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +0 -198
- package/v2Containers/InAppWrapper/index.js +0 -148
- package/v2Containers/InAppWrapper/messages.js +0 -49
- package/v2Containers/InappAdvance/index.js +0 -1099
- package/v2Containers/InappAdvance/index.scss +0 -10
- package/v2Containers/InappAdvance/tests/index.test.js +0 -448
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useState, useEffect, useCallback, useMemo, useRef,
|
|
3
|
-
} from "react";
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
4
2
|
import isEmpty from 'lodash/isEmpty';
|
|
5
3
|
import get from 'lodash/get';
|
|
6
4
|
import { bindActionCreators } from "redux";
|
|
@@ -9,19 +7,21 @@ import { injectIntl, FormattedMessage } from "react-intl";
|
|
|
9
7
|
import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
|
|
10
8
|
import CapHeading from "@capillarytech/cap-ui-library/CapHeading";
|
|
11
9
|
import CapSpin from "@capillarytech/cap-ui-library/CapSpin";
|
|
10
|
+
import CapInput from "@capillarytech/cap-ui-library/CapInput";
|
|
12
11
|
import CapRadioGroup from "@capillarytech/cap-ui-library/CapRadioGroup";
|
|
13
12
|
import CapRow from "@capillarytech/cap-ui-library/CapRow";
|
|
14
13
|
import CapColumn from "@capillarytech/cap-ui-library/CapColumn";
|
|
15
14
|
import CapButton from "@capillarytech/cap-ui-library/CapButton";
|
|
15
|
+
import CapTab from "@capillarytech/cap-ui-library/CapTab";
|
|
16
16
|
import CapNotification from "@capillarytech/cap-ui-library/CapNotification";
|
|
17
|
-
import { makeSelectInApp, makeSelectAccount
|
|
17
|
+
import { makeSelectInApp, makeSelectAccount } from "./selectors";
|
|
18
18
|
import * as globalActions from '../Cap/actions';
|
|
19
19
|
import {
|
|
20
20
|
isLoadingMetaEntities,
|
|
21
21
|
makeSelectMetaEntities,
|
|
22
22
|
setInjectedTags,
|
|
23
23
|
selectCurrentOrgDetails,
|
|
24
|
-
selectLiquidStateDetails
|
|
24
|
+
selectLiquidStateDetails
|
|
25
25
|
} from "../Cap/selectors";
|
|
26
26
|
import * as inAppActions from "./actions";
|
|
27
27
|
import './index.scss';
|
|
@@ -40,7 +40,6 @@ import {
|
|
|
40
40
|
ANDROID,
|
|
41
41
|
BIG_PICTURE,
|
|
42
42
|
BIG_TEXT,
|
|
43
|
-
BIG_HTML,
|
|
44
43
|
DEEP_LINK,
|
|
45
44
|
DEVICE_SUPPORTED,
|
|
46
45
|
INAPP_BUTTON_TYPES,
|
|
@@ -49,22 +48,18 @@ import {
|
|
|
49
48
|
INITIAL_CTA_DATA,
|
|
50
49
|
IOS,
|
|
51
50
|
LAYOUT_RADIO_OPTIONS,
|
|
52
|
-
|
|
51
|
+
AI_CONTENT_BOT_DISABLED,
|
|
53
52
|
} from "./constants";
|
|
54
53
|
import { INAPP, SMS } from "../CreativesContainer/constants";
|
|
55
54
|
import {
|
|
56
55
|
ALL, TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
|
|
57
56
|
} from "../Whatsapp/constants";
|
|
57
|
+
import CapDeviceContent from "../../v2Components/CapDeviceContent";
|
|
58
58
|
import { getCdnUrl } from "../../utils/cdnTransformation";
|
|
59
59
|
import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
|
|
60
60
|
import { validateInAppContent } from "../../utils/commonUtils";
|
|
61
|
-
import
|
|
61
|
+
import ErrorInfoNote from "../../v2Components/ErrorInfoNote";
|
|
62
62
|
import formBuilderMessages from "../../v2Components/FormBuilder/messages";
|
|
63
|
-
import HTMLEditor from "../../v2Components/HtmlEditor";
|
|
64
|
-
import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
|
|
65
|
-
import { INAPP_EDITOR_TYPES } from "../InAppWrapper/constants";
|
|
66
|
-
import InappAdvanced from "../InappAdvance/index";
|
|
67
|
-
import { ErrorInfoNote } from "../../v2Components/ErrorInfoNote";
|
|
68
63
|
|
|
69
64
|
let editContent = {};
|
|
70
65
|
|
|
@@ -72,14 +67,13 @@ export const InApp = (props) => {
|
|
|
72
67
|
const {
|
|
73
68
|
intl,
|
|
74
69
|
actions,
|
|
75
|
-
globalActions,
|
|
76
70
|
isFullMode,
|
|
77
71
|
onCreateComplete,
|
|
78
72
|
params,
|
|
79
73
|
templateData = {},
|
|
80
|
-
defaultData = {},
|
|
81
74
|
editData = {},
|
|
82
75
|
accountData = {},
|
|
76
|
+
globalActions,
|
|
83
77
|
location,
|
|
84
78
|
getDefaultTags,
|
|
85
79
|
supportedTags,
|
|
@@ -87,19 +81,8 @@ export const InApp = (props) => {
|
|
|
87
81
|
injectedTags,
|
|
88
82
|
getFormData,
|
|
89
83
|
selectedOfferDetails,
|
|
90
|
-
fetchingLiquidValidation,
|
|
91
|
-
templateName,
|
|
92
|
-
getTemplateDetailsInProgress,
|
|
93
|
-
isEditInApp,
|
|
94
|
-
setIsLoadingContent,
|
|
95
|
-
query,
|
|
96
|
-
inAppCreateMode,
|
|
97
|
-
isGetFormData,
|
|
98
|
-
showTemplateName,
|
|
99
|
-
onValidationFail,
|
|
100
|
-
type,
|
|
101
|
-
forwardedTags,
|
|
102
84
|
currentOrgDetails,
|
|
85
|
+
fetchingLiquidValidation,
|
|
103
86
|
// TestAndPreviewSlidebox props
|
|
104
87
|
showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
|
|
105
88
|
handleTestAndPreview: propsHandleTestAndPreview,
|
|
@@ -112,7 +95,7 @@ export const InApp = (props) => {
|
|
|
112
95
|
const [templateMediaType, setTemplateMediaType] = useState(
|
|
113
96
|
INAPP_MEDIA_TYPES.TEXT
|
|
114
97
|
);
|
|
115
|
-
const [
|
|
98
|
+
const [templateName, setTemplateName] = useState("");
|
|
116
99
|
const [templateLayoutType, setTemplateLayoutType] = useState(
|
|
117
100
|
INAPP_MESSAGE_LAYOUT_TYPES.MODAL
|
|
118
101
|
);
|
|
@@ -126,25 +109,6 @@ export const InApp = (props) => {
|
|
|
126
109
|
const [templateMessageErrorIos, setTemplateMessageErrorIos] = useState(false);
|
|
127
110
|
const [templateTitleErrorAndroid, setTemplateTitleErrorAndroid] = useState(false);
|
|
128
111
|
const [templateTitleErrorIos, setTemplateTitleErrorIos] = useState(false);
|
|
129
|
-
// HTML Editor content state (for INAPP HTML variant)
|
|
130
|
-
// Initialize HTML content from edit data if available
|
|
131
|
-
const getInitialHtmlContent = (device) => {
|
|
132
|
-
const editContent = isFullMode
|
|
133
|
-
? get(editData?.templateDetails?.versions, `base.content`, {})
|
|
134
|
-
: get(templateData?.versions, `base.content`, {});
|
|
135
|
-
const deviceContent = editContent?.[device];
|
|
136
|
-
return deviceContent?.message || "";
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const [htmlContentAndroid, setHtmlContentAndroid] = useState(() => getInitialHtmlContent("ANDROID"));
|
|
140
|
-
const [htmlContentIos, setHtmlContentIos] = useState(() => getInitialHtmlContent("IOS"));
|
|
141
|
-
// Track if this is an HTML template (type === HTML or style === BIG_HTML)
|
|
142
|
-
const [isHTMLTemplate, setIsHTMLTemplate] = useState(false);
|
|
143
|
-
// Version to force HTMLEditor remount on layout changes
|
|
144
|
-
const [htmlEditorContentVersion, setHtmlEditorContentVersion] = useState(0);
|
|
145
|
-
// Refs to store latest content before layout changes
|
|
146
|
-
const htmlContentAndroidRef = useRef(htmlContentAndroid);
|
|
147
|
-
const htmlContentIosRef = useRef(htmlContentIos);
|
|
148
112
|
const [accountId, setAccountId] = useState("");
|
|
149
113
|
const [accessToken, setAccessToken] = useState("");
|
|
150
114
|
const [accountName, setAccountName] = useState("");
|
|
@@ -253,6 +217,9 @@ export const InApp = (props) => {
|
|
|
253
217
|
}
|
|
254
218
|
}, [propsHandleCloseTestAndPreview]);
|
|
255
219
|
const { accessibleFeatures = [] } = currentOrgDetails || {};
|
|
220
|
+
const isAiContentBotDisabled = accessibleFeatures?.includes(
|
|
221
|
+
AI_CONTENT_BOT_DISABLED
|
|
222
|
+
);
|
|
256
223
|
const [errorMessage, setErrorMessage] = useState({
|
|
257
224
|
STANDARD_ERROR_MSG: {
|
|
258
225
|
ANDROID: [],
|
|
@@ -280,7 +247,7 @@ export const InApp = (props) => {
|
|
|
280
247
|
// DEVICE_SUPPORTED is '1', which indicates if the particular account is supported, and '0' if the devive is not supported
|
|
281
248
|
//get deep link keys in an array
|
|
282
249
|
const deepLinkKeys = Object.values(JSON.parse(deepLinkObj || '{}'));
|
|
283
|
-
const keys = deepLinkKeys?.map((link) => ({
|
|
250
|
+
const keys = deepLinkKeys?.map((link) => ({label: link.name, value: link.link, title: link.link }));
|
|
284
251
|
setPanes(isAndroidSupported ? ANDROID : IOS);
|
|
285
252
|
setDeepLink(keys);
|
|
286
253
|
setAccountId(sourceAccountIdentifier);
|
|
@@ -305,32 +272,6 @@ export const InApp = (props) => {
|
|
|
305
272
|
};
|
|
306
273
|
}, [paramObj.id]);
|
|
307
274
|
|
|
308
|
-
// Initialize template name from defaultData (from wrapper) or editData/templateData
|
|
309
|
-
useEffect(() => {
|
|
310
|
-
const defaultTemplateName = defaultData?.['template-name'] || '';
|
|
311
|
-
if (defaultTemplateName && !isEditFlow) {
|
|
312
|
-
setTempName(defaultTemplateName);
|
|
313
|
-
// Call showTemplateName callback if provided (for header display)
|
|
314
|
-
if (isFullMode && showTemplateName && defaultTemplateName) {
|
|
315
|
-
showTemplateName({
|
|
316
|
-
formData: { 'template-name': defaultTemplateName },
|
|
317
|
-
onFormDataChange: (updatedFormData) => {
|
|
318
|
-
const newName = updatedFormData?.['template-name'] || '';
|
|
319
|
-
setTempName(newName);
|
|
320
|
-
if (showTemplateName) {
|
|
321
|
-
showTemplateName({
|
|
322
|
-
formData: { 'template-name': newName },
|
|
323
|
-
onFormDataChange: (formData) => {
|
|
324
|
-
setTempName(formData?.['template-name'] || '');
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}, [defaultData?.['template-name'], showTemplateName, isEditFlow]);
|
|
333
|
-
|
|
334
275
|
useEffect(() => {
|
|
335
276
|
const {
|
|
336
277
|
name = "",
|
|
@@ -338,48 +279,16 @@ export const InApp = (props) => {
|
|
|
338
279
|
createdAt = "",
|
|
339
280
|
} = isFullMode ? editData?.templateDetails || {} : templateData || {};
|
|
340
281
|
editContent = get(versions, `base.content`, {});
|
|
341
|
-
|
|
342
|
-
// LIBRARY MODE FIX: Backend stores bodyType as 'POPUP' but UI expects 'MODAL'
|
|
343
|
-
// Convert back to MODAL for display
|
|
344
|
-
if (!isFullMode && editContent) {
|
|
345
|
-
if (editContent.ANDROID?.bodyType === INAPP_MESSAGE_LAYOUT_TYPES.POPUP) {
|
|
346
|
-
editContent.ANDROID.bodyType = INAPP_MESSAGE_LAYOUT_TYPES.MODAL;
|
|
347
|
-
}
|
|
348
|
-
if (editContent.IOS?.bodyType === INAPP_MESSAGE_LAYOUT_TYPES.POPUP) {
|
|
349
|
-
editContent.IOS.bodyType = INAPP_MESSAGE_LAYOUT_TYPES.MODAL;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
282
|
if (editContent && !isEmpty(editContent)) {
|
|
354
283
|
setEditFlow(true);
|
|
355
|
-
|
|
284
|
+
setTemplateName(name);
|
|
356
285
|
setTemplateDate(createdAt);
|
|
357
286
|
setTemplateLayoutType(editContent?.ANDROID?.bodyType);
|
|
358
|
-
// Call showTemplateName callback when in edit mode + full mode to show template name header
|
|
359
|
-
if (showTemplateName && name) {
|
|
360
|
-
showTemplateName({
|
|
361
|
-
formData: { 'template-name': name },
|
|
362
|
-
onFormDataChange: (updatedFormData) => {
|
|
363
|
-
const newName = updatedFormData?.['template-name'] || '';
|
|
364
|
-
setTempName(newName);
|
|
365
|
-
if (showTemplateName) {
|
|
366
|
-
showTemplateName({
|
|
367
|
-
formData: { 'template-name': newName },
|
|
368
|
-
onFormDataChange: (formData) => {
|
|
369
|
-
setTempName(formData?.['template-name'] || '');
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
287
|
const androidContent = editContent?.ANDROID;
|
|
377
|
-
let androidIsHTML = false;
|
|
378
288
|
if (!isEmpty(androidContent)) {
|
|
379
289
|
const {
|
|
380
290
|
title: androidTitle = '',
|
|
381
291
|
message: androidMessage = '',
|
|
382
|
-
type: androidType = '',
|
|
383
292
|
ctas: androidCta = {},
|
|
384
293
|
expandableDetails: androidExpandableDetails = {},
|
|
385
294
|
} = androidContent || {};
|
|
@@ -389,30 +298,12 @@ export const InApp = (props) => {
|
|
|
389
298
|
ctas: androidCtas,
|
|
390
299
|
} = androidExpandableDetails || {};
|
|
391
300
|
const androidCtaLength = androidCtas?.length;
|
|
392
|
-
// Check if this is a Bee editor template
|
|
393
|
-
const isBEEeditor = get(androidContent, 'isBEEeditor');
|
|
394
|
-
const isBeeFreeTemplate = !isEmpty(androidTitle) && androidTitle.toLowerCase() === 'bee free template';
|
|
395
|
-
// Check if this is an HTML editor template (identified by special title)
|
|
396
|
-
const isHTMLEditorTemplate = !isEmpty(androidTitle) && androidTitle.toLowerCase() === 'html editor template';
|
|
397
|
-
// Check if this is an HTML template
|
|
398
|
-
// Prioritize HTML editor template title, then check type/style
|
|
399
|
-
// But exclude if it's a Bee editor template
|
|
400
|
-
androidIsHTML = isHTMLEditorTemplate || ((androidType === INAPP_MEDIA_TYPES.HTML || androidStyle === BIG_HTML)
|
|
401
|
-
&& !isBEEeditor
|
|
402
|
-
&& !isBeeFreeTemplate);
|
|
403
|
-
setIsHTMLTemplate(androidIsHTML);
|
|
404
|
-
|
|
405
301
|
setTitleAndroid(androidTitle);
|
|
406
302
|
setTemplateMessageAndroid(androidMessage);
|
|
407
|
-
// Initialize HTML content for HTMLEditor if feature is enabled and it's an HTML template
|
|
408
|
-
if (androidIsHTML) {
|
|
409
|
-
setHtmlContentAndroid(androidMessage);
|
|
410
|
-
}
|
|
411
303
|
setTemplateMediaType(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
: INAPP_MEDIA_TYPES.IMAGE
|
|
304
|
+
androidStyle === BIG_TEXT
|
|
305
|
+
? INAPP_MEDIA_TYPES.TEXT
|
|
306
|
+
: INAPP_MEDIA_TYPES.IMAGE
|
|
416
307
|
);
|
|
417
308
|
setInAppImageSrcAndroid(androidImage);
|
|
418
309
|
setDeepLinkValueAndroid(androidCta[0]?.actionLink || '');
|
|
@@ -429,7 +320,6 @@ export const InApp = (props) => {
|
|
|
429
320
|
const {
|
|
430
321
|
title: iosTitle = '',
|
|
431
322
|
message: iosMessage = '',
|
|
432
|
-
type: iosType = '',
|
|
433
323
|
ctas: iosCta = {},
|
|
434
324
|
expandableDetails: iosExpandableDetails = {},
|
|
435
325
|
} = iosContent || {};
|
|
@@ -439,53 +329,9 @@ export const InApp = (props) => {
|
|
|
439
329
|
ctas: iosCtas,
|
|
440
330
|
} = iosExpandableDetails || {};
|
|
441
331
|
const iosCtaLength = iosCtas?.length;
|
|
442
|
-
|
|
443
|
-
// Check if this is an HTML template (if Android wasn't HTML, check iOS)
|
|
444
|
-
// Note: androidIsHTML is in the outer scope from the Android content check above
|
|
445
|
-
if (!androidIsHTML) {
|
|
446
|
-
// Check if this is a Bee editor template
|
|
447
|
-
const isBEEeditor = get(iosContent, 'isBEEeditor');
|
|
448
|
-
const isBeeFreeTemplate = !isEmpty(iosTitle) && iosTitle.toLowerCase() === 'bee free template';
|
|
449
|
-
// Check if this is an HTML editor template (identified by special title)
|
|
450
|
-
const isHTMLEditorTemplate = !isEmpty(iosTitle) && iosTitle.toLowerCase() === 'html editor template';
|
|
451
|
-
// Check if this is an HTML template
|
|
452
|
-
// Prioritize HTML editor template title, then check type/style
|
|
453
|
-
// But exclude if it's a Bee editor template
|
|
454
|
-
const iosIsHTML = isHTMLEditorTemplate || ((iosType === INAPP_MEDIA_TYPES.HTML || iosStyle === BIG_HTML)
|
|
455
|
-
&& !isBEEeditor
|
|
456
|
-
&& !isBeeFreeTemplate);
|
|
457
|
-
setIsHTMLTemplate(iosIsHTML);
|
|
458
|
-
// Initialize HTML content for HTMLEditor if feature is enabled and it's an HTML template
|
|
459
|
-
if (iosIsHTML) {
|
|
460
|
-
setHtmlContentIos(iosMessage);
|
|
461
|
-
}
|
|
462
|
-
} else {
|
|
463
|
-
// If Android is HTML, also initialize iOS HTML content if available
|
|
464
|
-
if (androidIsHTML) {
|
|
465
|
-
setHtmlContentIos(iosMessage);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
332
|
setTitleIos(iosTitle);
|
|
470
333
|
setTemplateMessageIos(iosMessage);
|
|
471
|
-
|
|
472
|
-
if (!androidIsHTML) {
|
|
473
|
-
// Check if this is a Bee editor template
|
|
474
|
-
const isBEEeditor = get(iosContent, 'isBEEeditor');
|
|
475
|
-
const isBeeFreeTemplate = !isEmpty(iosTitle) && iosTitle.toLowerCase() === 'bee free template';
|
|
476
|
-
// Check if this is an HTML editor template (identified by special title)
|
|
477
|
-
const isHTMLEditorTemplate = !isEmpty(iosTitle) && iosTitle.toLowerCase() === 'html editor template';
|
|
478
|
-
// Check if this is an HTML template
|
|
479
|
-
// Prioritize HTML editor template title, then check type/style
|
|
480
|
-
const iosIsHTML = isHTMLEditorTemplate || ((iosType === INAPP_MEDIA_TYPES.HTML || iosStyle === BIG_HTML)
|
|
481
|
-
&& !isBEEeditor
|
|
482
|
-
&& !isBeeFreeTemplate);
|
|
483
|
-
if (iosIsHTML) {
|
|
484
|
-
setTemplateMediaType(INAPP_MEDIA_TYPES.HTML);
|
|
485
|
-
} else {
|
|
486
|
-
setTemplateMediaType(iosStyle === BIG_TEXT ? INAPP_MEDIA_TYPES.TEXT : INAPP_MEDIA_TYPES.IMAGE);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
334
|
+
setTemplateMediaType(iosStyle === BIG_TEXT ? INAPP_MEDIA_TYPES.TEXT : INAPP_MEDIA_TYPES.IMAGE);
|
|
489
335
|
setInAppImageSrcIos(iosImage);
|
|
490
336
|
setButtonTypeIos(iosCtaLength ? INAPP_BUTTON_TYPES.CTA : INAPP_BUTTON_TYPES.NONE);
|
|
491
337
|
setAddActionLinkIos(!isEmpty(iosCta) && true);
|
|
@@ -494,47 +340,12 @@ export const InApp = (props) => {
|
|
|
494
340
|
setCtaDataIos(getCtaObject(iosCtas));
|
|
495
341
|
}
|
|
496
342
|
}
|
|
497
|
-
} else {
|
|
498
|
-
// Explicitly set edit flow to false if there's no edit content
|
|
499
|
-
setEditFlow(false);
|
|
500
343
|
}
|
|
501
344
|
}, [editData.templateDetails || templateData]);
|
|
502
345
|
|
|
503
|
-
// Extract editor type from defaultData for stable reference
|
|
504
|
-
const editorType = useMemo(() => {
|
|
505
|
-
const type = defaultData?.['editor-type'];
|
|
506
|
-
return type;
|
|
507
|
-
}, [defaultData]);
|
|
508
|
-
|
|
509
|
-
// Separate effect for handling editor type from wrapper in create mode
|
|
510
|
-
useEffect(() => {
|
|
511
|
-
// Only process editor type if we're not in edit flow
|
|
512
|
-
if (!isEditFlow) {
|
|
513
|
-
if (editorType === INAPP_EDITOR_TYPES.HTML_EDITOR) {
|
|
514
|
-
setIsHTMLTemplate(true);
|
|
515
|
-
setTemplateMediaType(INAPP_MEDIA_TYPES.HTML);
|
|
516
|
-
} else if (editorType === INAPP_EDITOR_TYPES.DRAG_DROP_EDITOR) {
|
|
517
|
-
setIsHTMLTemplate(false);
|
|
518
|
-
} else if (!editorType) {
|
|
519
|
-
// If no editor type is set yet, ensure we start with false
|
|
520
|
-
setIsHTMLTemplate(false);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}, [editorType, isEditFlow]);
|
|
524
|
-
|
|
525
346
|
// tag Code start from here
|
|
526
347
|
useEffect(() => {
|
|
527
|
-
//
|
|
528
|
-
tagsFetchedRef.current = false;
|
|
529
|
-
|
|
530
|
-
// Only fetch tags if HTML Editor is not being used (legacy flow)
|
|
531
|
-
// For HTML Editor, tags will be fetched via handleOnTagsContextChange
|
|
532
|
-
if (isHTMLTemplate) {
|
|
533
|
-
// HTML Editor will handle tag fetching via onContextChange
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
//fetching tags for legacy editor
|
|
348
|
+
//fetching tags
|
|
538
349
|
const { type, module } = location.query || {};
|
|
539
350
|
const isEmbedded = type === EMBEDDED;
|
|
540
351
|
const context = isEmbedded ? module : DEFAULT;
|
|
@@ -549,7 +360,7 @@ export const InApp = (props) => {
|
|
|
549
360
|
query.context = getDefaultTags;
|
|
550
361
|
}
|
|
551
362
|
globalActions.fetchSchemaForEntity(query);
|
|
552
|
-
}, [
|
|
363
|
+
}, []);
|
|
553
364
|
|
|
554
365
|
useEffect(() => {
|
|
555
366
|
let tag = get(metaEntities, `tags.standard`, []);
|
|
@@ -560,17 +371,7 @@ export const InApp = (props) => {
|
|
|
560
371
|
updateTags(tag);
|
|
561
372
|
}, [metaEntities]);
|
|
562
373
|
|
|
563
|
-
|
|
564
|
-
const tagsFetchedRef = React.useRef(false);
|
|
565
|
-
|
|
566
|
-
const handleOnTagsContextChange = useCallback((data) => {
|
|
567
|
-
// This function is called when TagList needs to fetch tags
|
|
568
|
-
// It triggers the API call to /meta/TAG endpoint via fetchSchemaForEntity
|
|
569
|
-
// Only fetch if we haven't already fetched for this context
|
|
570
|
-
if (tagsFetchedRef.current) {
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
|
|
374
|
+
const handleOnTagsContextChange = (data) => {
|
|
574
375
|
const { type } = location.query || {};
|
|
575
376
|
const tempData = (data || '').toLowerCase();
|
|
576
377
|
const isEmbedded = type === EMBEDDED;
|
|
@@ -582,192 +383,259 @@ export const InApp = (props) => {
|
|
|
582
383
|
context,
|
|
583
384
|
embedded,
|
|
584
385
|
};
|
|
585
|
-
// Mark as fetched to prevent duplicate calls
|
|
586
|
-
tagsFetchedRef.current = true;
|
|
587
|
-
// Call the API via Redux action - this will trigger the saga which calls Api.fetchSchemaForEntity
|
|
588
|
-
// The API endpoint will be: /meta/TAG?query={...}
|
|
589
386
|
globalActions.fetchSchemaForEntity(query);
|
|
590
|
-
}
|
|
387
|
+
};
|
|
591
388
|
|
|
389
|
+
const templateDescErrorHandler = (value) => {
|
|
390
|
+
let errorMessage = false;
|
|
391
|
+
const { unsupportedTags, isBraceError } = validateTags({
|
|
392
|
+
content: value,
|
|
393
|
+
tagsParam: tags,
|
|
394
|
+
injectedTagsParams: injectedTags,
|
|
395
|
+
location,
|
|
396
|
+
tagModule: getDefaultTags,
|
|
397
|
+
}) || {};
|
|
398
|
+
if (value === '' && INAPP_MEDIA_TYPES.NONE) {
|
|
399
|
+
errorMessage = formatMessage(messages.emptyTemplateDescErrorMessage);
|
|
400
|
+
} else if (unsupportedTags?.length > 0) {
|
|
401
|
+
errorMessage = formatMessage(
|
|
402
|
+
globalMessages.unsupportedTagsValidationError,
|
|
403
|
+
{
|
|
404
|
+
unsupportedTags,
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (isBraceError) {
|
|
409
|
+
errorMessage = formatMessage(globalMessages.unbalanacedCurlyBraces);
|
|
410
|
+
}
|
|
411
|
+
return errorMessage;
|
|
412
|
+
};
|
|
592
413
|
|
|
414
|
+
const onTagSelect = (data, id) => {
|
|
415
|
+
if (id === 0) {
|
|
416
|
+
const tempTitle = `${panes === ANDROID ? titleAndroid : titleIos}{{${data}}}`;
|
|
417
|
+
if (panes === ANDROID) {
|
|
418
|
+
setTitleAndroid(tempTitle);
|
|
419
|
+
} else {
|
|
420
|
+
setTitleIos(tempTitle);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
const tempMsg = `${panes === ANDROID ? templateMessageAndroid : templateMessageIos}{{${data}}}`;
|
|
424
|
+
const error = templateDescErrorHandler(tempMsg);
|
|
425
|
+
if (panes === ANDROID) {
|
|
426
|
+
setTemplateMessageAndroid(tempMsg);
|
|
427
|
+
setTemplateMessageErrorAndroid(error);
|
|
428
|
+
} else {
|
|
429
|
+
setTemplateMessageIos(tempMsg);
|
|
430
|
+
setTemplateMessageErrorIos(error);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
593
434
|
// tag Code end
|
|
594
435
|
|
|
436
|
+
const onTemplateNameChange = ({ target: { value } }) => {
|
|
437
|
+
setTemplateName(value);
|
|
438
|
+
};
|
|
439
|
+
|
|
595
440
|
const onTemplateLayoutTypeChange = ({ target: { value } }) => {
|
|
596
|
-
// Update layout type and force HTMLEditor to remount with latest content
|
|
597
|
-
// Increment version to force remount - this ensures editor displays current content
|
|
598
441
|
setTemplateLayoutType(value);
|
|
599
|
-
setHtmlEditorContentVersion((prev) => prev + 1);
|
|
600
442
|
};
|
|
601
443
|
|
|
444
|
+
const onCopyTitleAndContent = () => {
|
|
445
|
+
if (panes === ANDROID) {
|
|
446
|
+
setTitleAndroid(titleIos);
|
|
447
|
+
setTemplateMessageAndroid(templateMessageIos);
|
|
448
|
+
} else {
|
|
449
|
+
setTitleIos(titleAndroid);
|
|
450
|
+
setTemplateMessageIos(templateMessageAndroid);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
602
453
|
|
|
454
|
+
const PANES = [
|
|
455
|
+
{
|
|
456
|
+
content: (
|
|
457
|
+
<CapDeviceContent
|
|
458
|
+
intl={intl}
|
|
459
|
+
location={location}
|
|
460
|
+
injectedTags={injectIntl}
|
|
461
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
462
|
+
panes={panes}
|
|
463
|
+
actions={actions}
|
|
464
|
+
editData={editData}
|
|
465
|
+
isFullMode={isFullMode}
|
|
466
|
+
inAppImageSrc={inAppImageSrcAndroid}
|
|
467
|
+
setInAppImageSrc={setInAppImageSrcAndroid}
|
|
468
|
+
isEditFlow={isEditFlow}
|
|
469
|
+
ctaData={ctaDataAndroid}
|
|
470
|
+
setCtaData={setCtaDataAndroid}
|
|
471
|
+
buttonType={buttonTypeAndroid}
|
|
472
|
+
setButtonType={setButtonTypeAndroid}
|
|
473
|
+
accountId={accountId}
|
|
474
|
+
accessToken={accessToken}
|
|
475
|
+
templateMediaType={templateMediaType}
|
|
476
|
+
setTemplateMediaType={setTemplateMediaType}
|
|
477
|
+
title={titleAndroid}
|
|
478
|
+
setTitle={setTitleAndroid}
|
|
479
|
+
templateMessageError={templateMessageErrorAndroid}
|
|
480
|
+
templateMessage={templateMessageAndroid}
|
|
481
|
+
setTemplateMessage={setTemplateMessageAndroid}
|
|
482
|
+
setTemplateMessageError={setTemplateMessageErrorAndroid}
|
|
483
|
+
templateTitleError={templateTitleErrorAndroid}
|
|
484
|
+
setTemplateTitleError={setTemplateTitleErrorAndroid}
|
|
485
|
+
addActionLink={addActionLinkAndroid}
|
|
486
|
+
setAddActionLink={setAddActionLinkAndroid}
|
|
487
|
+
deepLink={deepLink}
|
|
488
|
+
deepLinkValue={deepLinkValueAndroid}
|
|
489
|
+
setDeepLinkValue={setDeepLinkValueAndroid}
|
|
490
|
+
onCopyTitleAndContent={onCopyTitleAndContent}
|
|
491
|
+
tags={tags}
|
|
492
|
+
onTagSelect={onTagSelect}
|
|
493
|
+
handleOnTagsContextChange={handleOnTagsContextChange}
|
|
494
|
+
templateDescErrorHandler={templateDescErrorHandler}
|
|
495
|
+
isAiContentBotDisabled={isAiContentBotDisabled}
|
|
496
|
+
/>
|
|
497
|
+
),
|
|
498
|
+
tab: <FormattedMessage {...messages.Android} />,
|
|
499
|
+
key: ANDROID,
|
|
500
|
+
isSupported: get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED,
|
|
501
|
+
// DEVICE_SUPPORTED is '1', which indicates if the particular account is supported, and '0' if the devive is not supported
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
content: (
|
|
505
|
+
<CapDeviceContent
|
|
506
|
+
intl={intl}
|
|
507
|
+
location={location}
|
|
508
|
+
injectedTags={injectIntl}
|
|
509
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
510
|
+
panes={panes}
|
|
511
|
+
actions={actions}
|
|
512
|
+
editData={editData}
|
|
513
|
+
isFullMode={isFullMode}
|
|
514
|
+
inAppImageSrc={inAppImageSrcIos}
|
|
515
|
+
setInAppImageSrc={setInAppImageSrcIos}
|
|
516
|
+
isEditFlow={isEditFlow}
|
|
517
|
+
ctaData={ctaDataIos}
|
|
518
|
+
setCtaData={setCtaDataIos}
|
|
519
|
+
buttonType={buttonTypeIos}
|
|
520
|
+
setButtonType={setButtonTypeIos}
|
|
521
|
+
accountId={accountId}
|
|
522
|
+
accessToken={accessToken}
|
|
523
|
+
templateMediaType={templateMediaType}
|
|
524
|
+
setTemplateMediaType={setTemplateMediaType}
|
|
525
|
+
title={titleIos}
|
|
526
|
+
setTitle={setTitleIos}
|
|
527
|
+
templateMessageError={templateMessageErrorIos}
|
|
528
|
+
templateMessage={templateMessageIos}
|
|
529
|
+
setTemplateMessage={setTemplateMessageIos}
|
|
530
|
+
setTemplateMessageError={setTemplateMessageErrorIos}
|
|
531
|
+
templateTitleError={templateTitleErrorIos}
|
|
532
|
+
setTemplateTitleError={setTemplateTitleErrorIos}
|
|
533
|
+
addActionLink={addActionLinkIos}
|
|
534
|
+
setAddActionLink={setAddActionLinkIos}
|
|
535
|
+
deepLink={deepLink}
|
|
536
|
+
deepLinkValue={deepLinkValueIos}
|
|
537
|
+
setDeepLinkValue={setDeepLinkValueIos}
|
|
538
|
+
onCopyTitleAndContent={onCopyTitleAndContent}
|
|
539
|
+
tags={tags}
|
|
540
|
+
onTagSelect={onTagSelect}
|
|
541
|
+
handleOnTagsContextChange={handleOnTagsContextChange}
|
|
542
|
+
templateDescErrorHandler={templateDescErrorHandler}
|
|
543
|
+
isAiContentBotDisabled={isAiContentBotDisabled}
|
|
544
|
+
/>
|
|
545
|
+
),
|
|
546
|
+
tab: <FormattedMessage {...messages.Ios} />,
|
|
547
|
+
key: IOS,
|
|
548
|
+
isSupported: get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED,
|
|
549
|
+
},
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
const createModeContent = (
|
|
553
|
+
<CapRow>
|
|
554
|
+
{/* template name */}
|
|
555
|
+
<CapHeading type="h4">
|
|
556
|
+
<FormattedMessage {...messages.creativeName} />
|
|
557
|
+
</CapHeading>
|
|
558
|
+
<CapInput
|
|
559
|
+
id="inapp-template-name-input"
|
|
560
|
+
className="inapp-template-name-input"
|
|
561
|
+
onChange={onTemplateNameChange}
|
|
562
|
+
placeholder={formatMessage(globalMessages.templateNamePlaceholder)}
|
|
563
|
+
value={templateName}
|
|
564
|
+
size="default"
|
|
565
|
+
/>
|
|
566
|
+
</CapRow>
|
|
567
|
+
);
|
|
603
568
|
//create methods end
|
|
604
569
|
|
|
605
570
|
//used by create and edit
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
// Get account-level device support restrictions
|
|
620
|
-
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
621
|
-
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
622
|
-
|
|
623
|
-
// Check if devices have content - ensure content is a string before calling trim
|
|
624
|
-
let hasAndroidContent = htmlContentAndroid && typeof htmlContentAndroid === 'string' && htmlContentAndroid?.trim() !== '';
|
|
625
|
-
let hasIosContent = htmlContentIos && typeof htmlContentIos === 'string' && htmlContentIos?.trim() !== '';
|
|
626
|
-
|
|
627
|
-
// LIBRARY MODE FIX: In library mode edit, htmlContent states might not be set yet
|
|
628
|
-
// because HTMLEditor loads content via initialContent prop, not via onContentChange
|
|
629
|
-
// Fallback to checking template data content
|
|
630
|
-
if (!hasAndroidContent && isEditFlow && !isFullMode && templateData) {
|
|
631
|
-
const androidTemplateContent = get(templateData, 'versions.base.content.ANDROID.message')
|
|
632
|
-
|| get(templateData, 'versions.base.content.ANDROID.beeHtml.value');
|
|
633
|
-
hasAndroidContent = androidTemplateContent && androidTemplateContent.trim() !== '';
|
|
634
|
-
}
|
|
635
|
-
if (!hasIosContent && isEditFlow && !isFullMode && templateData) {
|
|
636
|
-
const iosTemplateContent = get(templateData, 'versions.base.content.IOS.message')
|
|
637
|
-
|| get(templateData, 'versions.base.content.IOS.beeHtml.value');
|
|
638
|
-
hasIosContent = iosTemplateContent && iosTemplateContent.trim() !== '';
|
|
571
|
+
const getPreviewSection = () => {
|
|
572
|
+
const templateTitle = panes === ANDROID ? titleAndroid : titleIos;
|
|
573
|
+
const templateMsg = panes === ANDROID ? templateMessageAndroid : templateMessageIos;
|
|
574
|
+
const mediaPreview = {};
|
|
575
|
+
let ctaData = {};
|
|
576
|
+
if (panes === ANDROID) {
|
|
577
|
+
ctaData = ctaDataAndroid;
|
|
578
|
+
switch (templateMediaType) {
|
|
579
|
+
case INAPP_MEDIA_TYPES.IMAGE:
|
|
580
|
+
mediaPreview.inAppImageSrcAndroid = inAppImageSrcAndroid;
|
|
581
|
+
break;
|
|
582
|
+
default:
|
|
583
|
+
break;
|
|
639
584
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
return false;
|
|
650
|
-
}
|
|
651
|
-
// Android not supported and no content in template - skip validation
|
|
652
|
-
return false;
|
|
653
|
-
}
|
|
654
|
-
if (isIosDevice) {
|
|
655
|
-
// Only validate iOS if it's supported by account
|
|
656
|
-
// But in library mode edit, check template data too
|
|
657
|
-
if (iosSupported || (isEditFlow && !isFullMode && hasIosContent)) {
|
|
658
|
-
if (!hasIosContent) {
|
|
659
|
-
return true;
|
|
660
|
-
}
|
|
661
|
-
return false;
|
|
662
|
-
}
|
|
663
|
-
// iOS not supported and no content in template - skip validation
|
|
664
|
-
return false;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// If no specific device, check if at least one supported device has content
|
|
668
|
-
// Users can create templates with content in Android-only, iOS-only, or both devices
|
|
669
|
-
// Even when both devices are supported, user can create template with content in just one device
|
|
670
|
-
if (androidSupported && iosSupported) {
|
|
671
|
-
// Both devices supported - user can create template with content in Android, iOS, or both
|
|
672
|
-
// Only disable if NEITHER device has content
|
|
673
|
-
if (!hasAndroidContent && !hasIosContent) {
|
|
674
|
-
return true;
|
|
675
|
-
}
|
|
676
|
-
// At least one device has content - enable Done button
|
|
677
|
-
return false;
|
|
678
|
-
}
|
|
679
|
-
if (androidSupported) {
|
|
680
|
-
// Only Android supported - require Android content
|
|
681
|
-
if (!hasAndroidContent) {
|
|
682
|
-
return true;
|
|
683
|
-
}
|
|
684
|
-
// Android has content - enable Done button
|
|
685
|
-
return false;
|
|
686
|
-
}
|
|
687
|
-
if (iosSupported) {
|
|
688
|
-
// Only iOS supported - require iOS content
|
|
689
|
-
if (!hasIosContent) {
|
|
690
|
-
return true;
|
|
691
|
-
}
|
|
692
|
-
// iOS has content - enable Done button
|
|
693
|
-
return false;
|
|
585
|
+
} else {
|
|
586
|
+
ctaData = ctaDataIos;
|
|
587
|
+
switch (templateMediaType) {
|
|
588
|
+
case INAPP_MEDIA_TYPES.IMAGE:
|
|
589
|
+
mediaPreview.inAppImageSrcIos = inAppImageSrcIos;
|
|
590
|
+
break;
|
|
591
|
+
default:
|
|
592
|
+
break;
|
|
694
593
|
}
|
|
695
|
-
// Neither device supported - this shouldn't happen, but handle gracefully
|
|
696
|
-
return true;
|
|
697
594
|
}
|
|
595
|
+
return (
|
|
596
|
+
<UnifiedPreview
|
|
597
|
+
channel={INAPP}
|
|
598
|
+
content={{
|
|
599
|
+
inAppPreviewContent: {
|
|
600
|
+
mediaPreview,
|
|
601
|
+
templateTitle,
|
|
602
|
+
templateMsg,
|
|
603
|
+
...((isBtnTypeCtaAndroid || isBtnTypeCTaIos) && {
|
|
604
|
+
ctaData,
|
|
605
|
+
}),
|
|
606
|
+
},
|
|
607
|
+
templateLayoutType,
|
|
608
|
+
}}
|
|
609
|
+
device={panes}
|
|
610
|
+
showDeviceToggle={false}
|
|
611
|
+
showHeader={false}
|
|
612
|
+
formatMessage={formatMessage}
|
|
613
|
+
/>
|
|
614
|
+
);
|
|
615
|
+
};
|
|
698
616
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
// If no tags are available (e.g., in tests), allow submission even with minimal content
|
|
705
|
-
// This is to support test scenarios where full content validation might not be set up
|
|
706
|
-
const hasTags = tags && tags.length > 0;
|
|
707
|
-
const isTestScenario = !hasTags;
|
|
708
|
-
|
|
709
|
-
// If no device specified, check if at least one supported device has valid content
|
|
710
|
-
if (isNoDevice) {
|
|
711
|
-
// In test scenarios, only require message content (title is optional)
|
|
712
|
-
// In production, require both title and message
|
|
713
|
-
const hasAndroidContent = androidSupported && templateMessageAndroid && templateMessageAndroid?.trim() !== '' && !templateMessageErrorAndroid && (isTestScenario || (titleAndroid && titleAndroid?.trim() !== '' && !templateTitleErrorAndroid));
|
|
714
|
-
const hasIosContent = iosSupported && templateMessageIos && templateMessageIos?.trim() !== '' && !templateMessageErrorIos && (isTestScenario || (titleIos && titleIos?.trim() !== '' && !templateTitleErrorIos));
|
|
715
|
-
|
|
716
|
-
// Check media requirements
|
|
717
|
-
const androidMediaValid = !androidSupported || (templateMediaType === INAPP_MEDIA_TYPES.TEXT || inAppImageSrcAndroid !== '');
|
|
718
|
-
const iosMediaValid = !iosSupported || (templateMediaType === INAPP_MEDIA_TYPES.TEXT || inAppImageSrcIos !== '');
|
|
719
|
-
|
|
720
|
-
// Check CTA requirements
|
|
721
|
-
const androidCtaValid = !isBtnTypeCtaAndroid || (ctaDataAndroid[0]?.isSaved);
|
|
722
|
-
const iosCtaValid = !isBtnTypeCTaIos || (ctaDataIos[0]?.isSaved);
|
|
723
|
-
|
|
724
|
-
// Check action link requirements
|
|
725
|
-
const androidActionLinkValid = !addActionLinkAndroid || deepLinkValueAndroid;
|
|
726
|
-
const iosActionLinkValid = !addActionLinkIos || deepLinkValueIos;
|
|
727
|
-
|
|
728
|
-
// If both devices are supported, at least one should have valid content
|
|
729
|
-
if (androidSupported && iosSupported) {
|
|
730
|
-
const androidValid = hasAndroidContent && androidMediaValid && androidCtaValid && androidActionLinkValid;
|
|
731
|
-
const iosValid = hasIosContent && iosMediaValid && iosCtaValid && iosActionLinkValid;
|
|
732
|
-
return !(androidValid || iosValid);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// If only Android is supported, it must have valid content
|
|
736
|
-
if (androidSupported) {
|
|
737
|
-
return !(hasAndroidContent && androidMediaValid && androidCtaValid && androidActionLinkValid);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// If only iOS is supported, it must have valid content
|
|
741
|
-
if (iosSupported) {
|
|
742
|
-
return !(hasIosContent && iosMediaValid && iosCtaValid && iosActionLinkValid);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Neither device supported - disable
|
|
617
|
+
const isDisableDone = (device) => {
|
|
618
|
+
const isIosDevice = device === IOS;
|
|
619
|
+
const isAndroidDevice = device === ANDROID;
|
|
620
|
+
//if template name is not entered
|
|
621
|
+
if (isFullMode && templateName.trim() === '') {
|
|
746
622
|
return true;
|
|
747
623
|
}
|
|
748
|
-
|
|
749
624
|
//if template message is not entered
|
|
750
625
|
//for android
|
|
751
|
-
if (isAndroidDevice) {
|
|
752
|
-
|
|
753
|
-
if (androidMessage?.trim() === '' || templateMessageErrorAndroid) {
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
//for ios
|
|
758
|
-
if (isIosDevice) {
|
|
759
|
-
const iosMessage = templateMessageIos;
|
|
760
|
-
if (iosMessage?.trim() === '' || templateMessageErrorIos) {
|
|
761
|
-
return true;
|
|
762
|
-
}
|
|
626
|
+
if (isAndroidDevice && (templateMessageAndroid.trim() === '' || templateMessageErrorAndroid)) {
|
|
627
|
+
return true;
|
|
763
628
|
}
|
|
629
|
+
if (isIosDevice && (templateMessageIos.trim() === '' || templateMessageErrorIos)) {
|
|
630
|
+
return true;
|
|
631
|
+
}//for ios
|
|
764
632
|
|
|
765
633
|
//if template title is not entered
|
|
766
634
|
//for android
|
|
767
|
-
if (isAndroidDevice && (titleAndroid
|
|
635
|
+
if (isAndroidDevice && (titleAndroid.trim() === '' || templateTitleErrorAndroid)) {
|
|
768
636
|
return true;
|
|
769
637
|
}
|
|
770
|
-
if (isIosDevice && (titleIos
|
|
638
|
+
if (isIosDevice && (titleIos.trim() === '' || templateTitleErrorIos)) {
|
|
771
639
|
return true;
|
|
772
640
|
}//for ios
|
|
773
641
|
//if media type is image and the mediaType file is not uploaded
|
|
@@ -826,11 +694,11 @@ export const InApp = (props) => {
|
|
|
826
694
|
switch (templateMediaType) {
|
|
827
695
|
case INAPP_MEDIA_TYPES.IMAGE:
|
|
828
696
|
androidMediaParams = {
|
|
829
|
-
image: getCdnUrl({
|
|
697
|
+
image: getCdnUrl({url: inAppImageSrcAndroid, channelName: INAPP }),
|
|
830
698
|
style: BIG_PICTURE,
|
|
831
699
|
};
|
|
832
700
|
iosMediaParams = {
|
|
833
|
-
image: getCdnUrl({
|
|
701
|
+
image: getCdnUrl({url: inAppImageSrcIos, channelName: INAPP }),
|
|
834
702
|
style: BIG_PICTURE,
|
|
835
703
|
};
|
|
836
704
|
break;
|
|
@@ -842,46 +710,16 @@ export const InApp = (props) => {
|
|
|
842
710
|
sourceAccountIdentifier = "",
|
|
843
711
|
id,
|
|
844
712
|
} = accountObj;
|
|
845
|
-
|
|
846
|
-
//
|
|
847
|
-
const
|
|
848
|
-
? htmlContentAndroid
|
|
849
|
-
: templateMessageAndroid;
|
|
850
|
-
|
|
851
|
-
// Determine type and style for Android
|
|
852
|
-
const androidType = isHTMLTemplate ? INAPP_MEDIA_TYPES.HTML : templateMediaType;
|
|
853
|
-
const androidExpandableStyle = isHTMLTemplate ? BIG_HTML : BIG_TEXT;
|
|
854
|
-
|
|
855
|
-
// LIBRARY MODE FIX: Backend doesn't support 'MODAL' bodyType, convert to 'POPUP'
|
|
856
|
-
const bodyTypeForBackend = templateLayoutType === INAPP_MESSAGE_LAYOUT_TYPES.MODAL ? INAPP_MESSAGE_LAYOUT_TYPES.POPUP : templateLayoutType;
|
|
857
|
-
|
|
858
|
-
// Check account-level device support
|
|
859
|
-
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
860
|
-
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
861
|
-
|
|
862
|
-
// Check if devices have content (for HTML Editor)
|
|
863
|
-
const hasAndroidContent = htmlContentAndroid && htmlContentAndroid?.trim() !== '';
|
|
864
|
-
const hasIosContent = htmlContentIos && htmlContentIos?.trim() !== '';
|
|
865
|
-
|
|
866
|
-
// For HTML Editor, check if device has content and is supported
|
|
867
|
-
// For legacy editor, use isDisableDone check
|
|
868
|
-
// Only include devices that have content - allows Android-only, iOS-only, or both
|
|
869
|
-
const shouldIncludeAndroid = isHTMLTemplate
|
|
870
|
-
? (androidSupported && hasAndroidContent)
|
|
871
|
-
: !isDisableDone(ANDROID);
|
|
872
|
-
|
|
873
|
-
// Construct Android content if device is supported and has content
|
|
874
|
-
// Even when both devices are supported, only include devices that have content
|
|
875
|
-
const androidContent = shouldIncludeAndroid ? {
|
|
713
|
+
|
|
714
|
+
// Construct Android content if not disabled
|
|
715
|
+
const androidContent = !isDisableDone(ANDROID) ? {
|
|
876
716
|
...commonDevicePayload,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
title: isHTMLTemplate ? 'html editor template' : titleAndroid,
|
|
880
|
-
message: androidMessage,
|
|
717
|
+
title: titleAndroid,
|
|
718
|
+
message: templateMessageAndroid,
|
|
881
719
|
bodyType: templateLayoutType,
|
|
882
720
|
expandableDetails: {
|
|
883
|
-
style:
|
|
884
|
-
message:
|
|
721
|
+
style: BIG_TEXT,
|
|
722
|
+
message: templateMessageAndroid,
|
|
885
723
|
...androidMediaParams,
|
|
886
724
|
...(isBtnTypeCtaAndroid && {
|
|
887
725
|
ctas: getCtaPayload(ANDROID),
|
|
@@ -889,38 +727,19 @@ export const InApp = (props) => {
|
|
|
889
727
|
},
|
|
890
728
|
custom: [],
|
|
891
729
|
...(deepLinkValueAndroid && {
|
|
892
|
-
ctas: [{
|
|
730
|
+
ctas: [{type: DEEP_LINK, actionLink: deepLinkValueAndroid}],
|
|
893
731
|
}),
|
|
894
732
|
} : {};
|
|
895
|
-
|
|
896
|
-
//
|
|
897
|
-
const
|
|
898
|
-
? htmlContentIos
|
|
899
|
-
: templateMessageIos;
|
|
900
|
-
|
|
901
|
-
// Determine type and style for iOS
|
|
902
|
-
const iosType = isHTMLTemplate ? INAPP_MEDIA_TYPES.HTML : templateMediaType;
|
|
903
|
-
const iosExpandableStyle = isHTMLTemplate ? BIG_HTML : BIG_TEXT;
|
|
904
|
-
|
|
905
|
-
// For HTML Editor, check if device has content and is supported
|
|
906
|
-
// For legacy editor, use isDisableDone check
|
|
907
|
-
// Only include devices that have content - allows Android-only, iOS-only, or both
|
|
908
|
-
const shouldIncludeIos = isHTMLTemplate
|
|
909
|
-
? (iosSupported && hasIosContent)
|
|
910
|
-
: !isDisableDone(IOS);
|
|
911
|
-
|
|
912
|
-
// Construct iOS content if device is supported and has content
|
|
913
|
-
// Even when both devices are supported, only include devices that have content
|
|
914
|
-
const iosContent = shouldIncludeIos ? {
|
|
733
|
+
|
|
734
|
+
// Construct iOS content if not disabled
|
|
735
|
+
const iosContent = !isDisableDone(IOS) ? {
|
|
915
736
|
...commonDevicePayload,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
message: iosMessage,
|
|
920
|
-
bodyType: bodyTypeForBackend,
|
|
737
|
+
title: titleIos,
|
|
738
|
+
message: templateMessageIos,
|
|
739
|
+
bodyType: templateLayoutType,
|
|
921
740
|
expandableDetails: {
|
|
922
|
-
style:
|
|
923
|
-
message:
|
|
741
|
+
style: BIG_TEXT,
|
|
742
|
+
message: templateMessageIos,
|
|
924
743
|
...iosMediaParams,
|
|
925
744
|
...(isBtnTypeCTaIos && {
|
|
926
745
|
ctas: getCtaPayload(IOS),
|
|
@@ -928,16 +747,12 @@ export const InApp = (props) => {
|
|
|
928
747
|
},
|
|
929
748
|
custom: [],
|
|
930
749
|
...(deepLinkValueIos && {
|
|
931
|
-
ctas: [{
|
|
750
|
+
ctas: [{type: DEEP_LINK, actionLink: deepLinkValueIos}],
|
|
932
751
|
}),
|
|
933
752
|
} : {};
|
|
934
|
-
|
|
935
|
-
// Ensure name is always set - use tempName as fallback if templateName is empty
|
|
936
|
-
const templateNameValue = isEditFlow ? tempName : (templateName || tempName || '');
|
|
937
|
-
const trimmedName = (templateNameValue && typeof templateNameValue === 'string') ? templateNameValue?.trim() : '';
|
|
938
|
-
|
|
753
|
+
|
|
939
754
|
const data = {
|
|
940
|
-
name:
|
|
755
|
+
name: templateName,
|
|
941
756
|
versions: {
|
|
942
757
|
base: {
|
|
943
758
|
content: {
|
|
@@ -956,26 +771,28 @@ export const InApp = (props) => {
|
|
|
956
771
|
return data;
|
|
957
772
|
};
|
|
958
773
|
|
|
959
|
-
const actionCallback = ({
|
|
960
|
-
if (!
|
|
774
|
+
const actionCallback = ({ errorMessage }) => {
|
|
775
|
+
if (!errorMessage) {
|
|
961
776
|
CapNotification.success({
|
|
962
|
-
message: isEditFlow
|
|
963
|
-
|
|
964
|
-
|
|
777
|
+
message: isEditFlow ? formatMessage(messages.inAppEditNotification, {
|
|
778
|
+
name: templateName,
|
|
779
|
+
}) : formatMessage(messages.inAppCreateNotification, {
|
|
780
|
+
name: templateName,
|
|
781
|
+
}),
|
|
965
782
|
});
|
|
966
783
|
actions.clearCreateResponse();
|
|
967
784
|
} else {
|
|
968
785
|
CapNotification.error({
|
|
969
|
-
message: JSON.stringify(
|
|
786
|
+
message: JSON.stringify(errorMessage),
|
|
970
787
|
});
|
|
971
788
|
}
|
|
972
789
|
};
|
|
973
790
|
|
|
974
791
|
const onCreateInApp = () => {
|
|
975
792
|
setSpin(true);
|
|
976
|
-
actions.createInAppTemplate(createPayload(), (resp,
|
|
977
|
-
actionCallback({ resp,
|
|
978
|
-
if (!
|
|
793
|
+
actions.createInAppTemplate(createPayload(), (resp, errorMessage) => {
|
|
794
|
+
actionCallback({ resp, errorMessage });
|
|
795
|
+
if (!errorMessage) {
|
|
979
796
|
onCreateComplete();
|
|
980
797
|
} else {
|
|
981
798
|
setSpin(false);
|
|
@@ -990,9 +807,9 @@ export const InApp = (props) => {
|
|
|
990
807
|
...createPayload(),
|
|
991
808
|
_id: params.id,
|
|
992
809
|
},
|
|
993
|
-
(resp,
|
|
994
|
-
actionCallback({ resp,
|
|
995
|
-
if (!
|
|
810
|
+
(resp, errorMessage) => {
|
|
811
|
+
actionCallback({ resp, errorMessage });
|
|
812
|
+
if (!errorMessage) {
|
|
996
813
|
onCreateComplete();
|
|
997
814
|
} else {
|
|
998
815
|
setSpin(false);
|
|
@@ -1001,67 +818,6 @@ export const InApp = (props) => {
|
|
|
1001
818
|
);
|
|
1002
819
|
};
|
|
1003
820
|
|
|
1004
|
-
// Handle HTML content changes from HTMLEditor
|
|
1005
|
-
const handleHtmlContentChange = useCallback((deviceContent, changedDevice) => {
|
|
1006
|
-
// The onChange callback from useInAppContent passes the full deviceContent object
|
|
1007
|
-
// But we only want to update the state for the device that actually changed
|
|
1008
|
-
// Use the second parameter (changedDevice) if provided, otherwise update both
|
|
1009
|
-
|
|
1010
|
-
// Clear validation errors when content changes (similar to Bee Editor)
|
|
1011
|
-
// This ensures Done button re-enables after user fixes errors
|
|
1012
|
-
if (changedDevice) {
|
|
1013
|
-
setErrorMessage((prev) => ({
|
|
1014
|
-
STANDARD_ERROR_MSG: {
|
|
1015
|
-
...prev.STANDARD_ERROR_MSG,
|
|
1016
|
-
[changedDevice.toUpperCase()]: [],
|
|
1017
|
-
GENERIC: [],
|
|
1018
|
-
},
|
|
1019
|
-
LIQUID_ERROR_MSG: {
|
|
1020
|
-
...prev.LIQUID_ERROR_MSG,
|
|
1021
|
-
[changedDevice.toUpperCase()]: [],
|
|
1022
|
-
GENERIC: [],
|
|
1023
|
-
},
|
|
1024
|
-
}));
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
if (changedDevice) {
|
|
1028
|
-
// Only update the device that actually changed
|
|
1029
|
-
if (changedDevice.toUpperCase() === ANDROID && deviceContent?.android !== undefined) {
|
|
1030
|
-
setHtmlContentAndroid(deviceContent.android || '');
|
|
1031
|
-
} else if (changedDevice.toUpperCase() === IOS_CAPITAL && deviceContent?.ios !== undefined) {
|
|
1032
|
-
setHtmlContentIos(deviceContent.ios || '');
|
|
1033
|
-
}
|
|
1034
|
-
} else {
|
|
1035
|
-
// Fallback: update both if changedDevice not provided (for backward compatibility)
|
|
1036
|
-
// Only update if value actually changed to avoid unnecessary re-renders
|
|
1037
|
-
if (deviceContent?.android !== undefined) {
|
|
1038
|
-
setHtmlContentAndroid((prev) => {
|
|
1039
|
-
const newValue = deviceContent.android || '';
|
|
1040
|
-
return prev !== newValue ? newValue : prev;
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
if (deviceContent?.ios !== undefined) {
|
|
1044
|
-
setHtmlContentIos((prev) => {
|
|
1045
|
-
const newValue = deviceContent.ios || '';
|
|
1046
|
-
return prev !== newValue ? newValue : prev;
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}, [ANDROID, IOS, setErrorMessage]);
|
|
1051
|
-
|
|
1052
|
-
// Handle HTML save from HTMLEditor
|
|
1053
|
-
const handleHtmlSave = useCallback((deviceContent) => {
|
|
1054
|
-
// Update state for both devices if present in the callback
|
|
1055
|
-
if (deviceContent?.android !== undefined) {
|
|
1056
|
-
setHtmlContentAndroid(deviceContent.android || '');
|
|
1057
|
-
}
|
|
1058
|
-
if (deviceContent?.ios !== undefined) {
|
|
1059
|
-
setHtmlContentIos(deviceContent.ios || '');
|
|
1060
|
-
}
|
|
1061
|
-
// Auto-save can trigger this, but we don't want to submit automatically
|
|
1062
|
-
// The user will click the Create/Update button to submit
|
|
1063
|
-
}, []);
|
|
1064
|
-
|
|
1065
821
|
const onDoneCallback = () => {
|
|
1066
822
|
if (isFullMode) {
|
|
1067
823
|
if (isEditFlow) {
|
|
@@ -1069,17 +825,15 @@ export const InApp = (props) => {
|
|
|
1069
825
|
}
|
|
1070
826
|
return onCreateInApp();
|
|
1071
827
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
value: payload,
|
|
828
|
+
return getFormData({
|
|
829
|
+
value: createPayload(),
|
|
1075
830
|
_id: params && params.id,
|
|
1076
831
|
validity: true,
|
|
1077
832
|
type: INAPP,
|
|
1078
833
|
});
|
|
1079
834
|
};
|
|
1080
835
|
|
|
1081
|
-
|
|
1082
|
-
const validationMiddleWare = async () => {
|
|
836
|
+
const liquidMiddleWare = () => {
|
|
1083
837
|
// Set up callbacks for validation results
|
|
1084
838
|
const onError = ({ standardErrors, liquidErrors }) => {
|
|
1085
839
|
setErrorMessage((prev) => ({
|
|
@@ -1092,200 +846,45 @@ export const InApp = (props) => {
|
|
|
1092
846
|
onDoneCallback();
|
|
1093
847
|
};
|
|
1094
848
|
|
|
1095
|
-
//
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
return skipRegexes.some((regex) => regex.test(tag));
|
|
1124
|
-
},
|
|
1125
|
-
singleTab: getSingleTab(accountData),
|
|
1126
|
-
});
|
|
1127
|
-
} else if (hasTags) {
|
|
1128
|
-
// For non-liquid flow, validate tags using validateTags (only if tags are available)
|
|
1129
|
-
const androidContent = htmlContentAndroid || '';
|
|
1130
|
-
const iosContent = htmlContentIos || '';
|
|
1131
|
-
|
|
1132
|
-
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
1133
|
-
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
1134
|
-
|
|
1135
|
-
let hasErrors = false;
|
|
1136
|
-
const newErrors = {
|
|
1137
|
-
STANDARD_ERROR_MSG: {
|
|
1138
|
-
ANDROID: [],
|
|
1139
|
-
IOS: [],
|
|
1140
|
-
GENERIC: [],
|
|
1141
|
-
},
|
|
1142
|
-
LIQUID_ERROR_MSG: {
|
|
1143
|
-
ANDROID: [],
|
|
1144
|
-
IOS: [],
|
|
1145
|
-
GENERIC: [],
|
|
1146
|
-
},
|
|
1147
|
-
};
|
|
1148
|
-
|
|
1149
|
-
// Validate Android content
|
|
1150
|
-
if (androidSupported && androidContent && androidContent?.trim() !== '') {
|
|
1151
|
-
const validationResponse = validateTags({
|
|
1152
|
-
content: androidContent,
|
|
1153
|
-
tagsParam: tags,
|
|
1154
|
-
injectedTagsParams: injectedTags || {},
|
|
1155
|
-
location,
|
|
1156
|
-
tagModule: getDefaultTags,
|
|
1157
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1158
|
-
}) || {};
|
|
1159
|
-
|
|
1160
|
-
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
1161
|
-
hasErrors = true;
|
|
1162
|
-
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
1163
|
-
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1164
|
-
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
1165
|
-
})
|
|
1166
|
-
);
|
|
1167
|
-
}
|
|
1168
|
-
if (validationResponse?.isBraceError) {
|
|
1169
|
-
hasErrors = true;
|
|
1170
|
-
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
1171
|
-
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Validate iOS content
|
|
1177
|
-
if (iosSupported && iosContent && iosContent?.trim() !== '') {
|
|
1178
|
-
const validationResponse = validateTags({
|
|
1179
|
-
content: iosContent,
|
|
1180
|
-
tagsParam: tags,
|
|
1181
|
-
injectedTagsParams: injectedTags || {},
|
|
1182
|
-
location,
|
|
1183
|
-
tagModule: getDefaultTags,
|
|
1184
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1185
|
-
}) || {};
|
|
1186
|
-
|
|
1187
|
-
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
1188
|
-
hasErrors = true;
|
|
1189
|
-
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
1190
|
-
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1191
|
-
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
1192
|
-
})
|
|
1193
|
-
);
|
|
1194
|
-
}
|
|
1195
|
-
if (validationResponse?.isBraceError) {
|
|
1196
|
-
hasErrors = true;
|
|
1197
|
-
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
1198
|
-
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
if (hasErrors) {
|
|
1204
|
-
setErrorMessage(newErrors);
|
|
1205
|
-
} else {
|
|
1206
|
-
// No errors, proceed with submission
|
|
1207
|
-
onSuccess();
|
|
1208
|
-
}
|
|
1209
|
-
} else {
|
|
1210
|
-
// No tags available, skip validation and proceed directly
|
|
1211
|
-
onSuccess();
|
|
1212
|
-
}
|
|
849
|
+
// Validate the INAPP content
|
|
850
|
+
const payload = createPayload();
|
|
851
|
+
validateInAppContent(payload, {
|
|
852
|
+
currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
|
|
853
|
+
onError,
|
|
854
|
+
onSuccess,
|
|
855
|
+
getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
|
|
856
|
+
formatMessage,
|
|
857
|
+
messages: formBuilderMessages,
|
|
858
|
+
tagLookupMap: metaEntities?.tagLookupMap || {},
|
|
859
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
860
|
+
isLiquidFlow,
|
|
861
|
+
forwardedTags: {},
|
|
862
|
+
skipTags: (tag) => {
|
|
863
|
+
// Skip certain tags if needed
|
|
864
|
+
const skipRegexes = [
|
|
865
|
+
/dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
|
|
866
|
+
/unsubscribe\(#[a-zA-Z\d]{6}\)/,
|
|
867
|
+
/Link_to_[a-zA-z]/,
|
|
868
|
+
/SURVEY.*\.TOKEN/,
|
|
869
|
+
/^[A-Za-z].*\([a-zA-Z\d]*\)/,
|
|
870
|
+
];
|
|
871
|
+
|
|
872
|
+
return skipRegexes.some((regex) => regex.test(tag));
|
|
873
|
+
},
|
|
874
|
+
singleTab: getSingleTab(accountData),
|
|
875
|
+
});
|
|
1213
876
|
};
|
|
1214
877
|
|
|
1215
|
-
const isLiquidFlow =
|
|
1216
|
-
|
|
1217
|
-
// Check template data to determine editor type (for render decision)
|
|
1218
|
-
const templateDetails = isFullMode ? editData?.templateDetails : templateData;
|
|
1219
|
-
const versions = templateDetails?.versions || {};
|
|
1220
|
-
const templateContent = get(versions, 'base.content', {});
|
|
1221
|
-
const androidContent = templateContent?.ANDROID || {};
|
|
1222
|
-
const iosContent = templateContent?.IOS || {};
|
|
1223
|
-
|
|
1224
|
-
// Check if this is a Bee editor template
|
|
1225
|
-
const isBEEeditor = get(androidContent, 'isBEEeditor') || get(iosContent, 'isBEEeditor');
|
|
1226
|
-
const androidTitle = androidContent?.title || '';
|
|
1227
|
-
const iosTitle = iosContent?.title || '';
|
|
1228
|
-
const isBeeFreeTemplate = (!isEmpty(androidTitle) && androidTitle.toLowerCase() === 'bee free template')
|
|
1229
|
-
|| (!isEmpty(iosTitle) && iosTitle.toLowerCase() === 'bee free template');
|
|
1230
|
-
|
|
1231
|
-
// Check if this is an HTML template from content data
|
|
1232
|
-
const androidType = androidContent?.type || '';
|
|
1233
|
-
const androidStyle = androidContent?.expandableDetails?.style || '';
|
|
1234
|
-
const iosType = iosContent?.type || '';
|
|
1235
|
-
const iosStyle = iosContent?.expandableDetails?.style || '';
|
|
1236
|
-
const isHTMLTemplateFromData = (androidType === INAPP_MEDIA_TYPES.HTML || androidStyle === BIG_HTML
|
|
1237
|
-
|| iosType === INAPP_MEDIA_TYPES.HTML || iosStyle === BIG_HTML)
|
|
1238
|
-
&& !isBEEeditor
|
|
1239
|
-
&& !isBeeFreeTemplate;
|
|
1240
|
-
|
|
1241
|
-
// Use state if available, otherwise fall back to direct data check
|
|
1242
|
-
const shouldUseHTMLEditor = isHTMLTemplate || isHTMLTemplateFromData;
|
|
1243
|
-
|
|
1244
|
-
// Only route to Bee editor if it's explicitly a Bee editor AND not an HTML template
|
|
1245
|
-
const shouldUseBeeEditor = (isBEEeditor || isBeeFreeTemplate) && !shouldUseHTMLEditor;
|
|
1246
|
-
|
|
1247
|
-
// Early returns to avoid nested ternary
|
|
1248
|
-
if (isEditInApp && getTemplateDetailsInProgress) {
|
|
1249
|
-
return <CapSpin spinning={getTemplateDetailsInProgress} />;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
// Allow BEE editor in both full mode edit (isEditInApp) AND library mode edit (isEditFlow && !isFullMode)
|
|
1253
|
-
if ((isEditInApp || (isEditFlow && !isFullMode)) && shouldUseBeeEditor) {
|
|
1254
|
-
return (
|
|
1255
|
-
<InappAdvanced
|
|
1256
|
-
getFormData={getFormData}
|
|
1257
|
-
setIsLoadingContent={setIsLoadingContent}
|
|
1258
|
-
defaultData={{ "template-name": tempName }}
|
|
1259
|
-
location={{
|
|
1260
|
-
pathname: `/inapp/edit`,
|
|
1261
|
-
query,
|
|
1262
|
-
search: '',
|
|
1263
|
-
}}
|
|
1264
|
-
params={{ mode: inAppCreateMode, id: params.id }}
|
|
1265
|
-
isFullMode={isFullMode}
|
|
1266
|
-
isGetFormData={isGetFormData}
|
|
1267
|
-
showTemplateName={showTemplateName}
|
|
1268
|
-
route={{ name: "inapp" }}
|
|
1269
|
-
getDefaultTags={type}
|
|
1270
|
-
onValidationFail={onValidationFail}
|
|
1271
|
-
templateData={templateData}
|
|
1272
|
-
templateName={tempName}
|
|
1273
|
-
setTemplateName={setTempName}
|
|
1274
|
-
forwardedTags={forwardedTags}
|
|
1275
|
-
selectedOfferDetails={selectedOfferDetails}
|
|
1276
|
-
onCreateComplete={onCreateComplete}
|
|
1277
|
-
/>
|
|
1278
|
-
);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
878
|
+
const isLiquidFlow = true;
|
|
1281
879
|
return (
|
|
1282
880
|
<CapSpin spinning={spin || fetchingLiquidValidation} tip={fetchingLiquidValidation ? <FormattedMessage {...formBuilderMessages.liquidSpinText} /> : ""}>
|
|
1283
881
|
<CapRow className="cap-inapp-creatives">
|
|
1284
|
-
<CapColumn span={
|
|
1285
|
-
{
|
|
1286
|
-
{
|
|
882
|
+
<CapColumn span={14}>
|
|
883
|
+
{isFullMode && createModeContent}
|
|
884
|
+
{/* Creative layout type*/}
|
|
885
|
+
{(isFullMode || !isEditFlow) && (
|
|
1287
886
|
<>
|
|
1288
|
-
<CapRow>
|
|
887
|
+
<CapRow className="inapp-creative-layout">
|
|
1289
888
|
<CapHeading type="h4">
|
|
1290
889
|
<FormattedMessage {...messages.creativeLayout} />
|
|
1291
890
|
</CapHeading>
|
|
@@ -1302,64 +901,26 @@ export const InApp = (props) => {
|
|
|
1302
901
|
/>
|
|
1303
902
|
</>
|
|
1304
903
|
)}
|
|
1305
|
-
{
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
// Tag insertion is handled by HTMLEditor's CodeEditorPane at cursor position
|
|
1322
|
-
// Content updates will be propagated via onContentChange callback
|
|
1323
|
-
}}
|
|
1324
|
-
// Don't pass globalActions to prevent duplicate API calls
|
|
1325
|
-
// HTMLEditor will use onContextChange instead
|
|
1326
|
-
onContextChange={handleOnTagsContextChange}
|
|
1327
|
-
// Pass validation errors to HTMLEditor for display
|
|
1328
|
-
errors={errorMessage}
|
|
1329
|
-
isFullMode={isFullMode}
|
|
1330
|
-
data-test="inapp-html-editor"
|
|
1331
|
-
style={{ width: '138%' }}
|
|
1332
|
-
/>
|
|
1333
|
-
) : (
|
|
1334
|
-
<InappAdvanced
|
|
1335
|
-
getFormData={getFormData}
|
|
1336
|
-
setIsLoadingContent={setIsLoadingContent}
|
|
1337
|
-
defaultData={{ "template-name": tempName }}
|
|
1338
|
-
location={{
|
|
1339
|
-
pathname: `/inapp/create`,
|
|
1340
|
-
query,
|
|
1341
|
-
search: '',
|
|
1342
|
-
}}
|
|
1343
|
-
params={{ mode: inAppCreateMode }}
|
|
1344
|
-
isFullMode={isFullMode}
|
|
1345
|
-
isGetFormData={isGetFormData}
|
|
1346
|
-
showTemplateName={showTemplateName}
|
|
1347
|
-
route={{ name: "inapp" }}
|
|
1348
|
-
getDefaultTags={type}
|
|
1349
|
-
onValidationFail={onValidationFail}
|
|
1350
|
-
templateData={templateData}
|
|
1351
|
-
templateName={tempName}
|
|
1352
|
-
setTemplateName={setTempName}
|
|
1353
|
-
forwardedTags={forwardedTags}
|
|
1354
|
-
selectedOfferDetails={selectedOfferDetails}
|
|
1355
|
-
onCreateComplete={onCreateComplete}
|
|
1356
|
-
/>
|
|
1357
|
-
)}
|
|
904
|
+
{/* device tab */}
|
|
905
|
+
<CapTab
|
|
906
|
+
panes={PANES.filter(
|
|
907
|
+
(devicePane) => devicePane?.isSupported === true
|
|
908
|
+
)}
|
|
909
|
+
onChange={(value) => {
|
|
910
|
+
setPanes(value);
|
|
911
|
+
}}
|
|
912
|
+
activeKey={panes}
|
|
913
|
+
defaultActiveKey={panes}
|
|
914
|
+
className="inapp-template-device-tab"
|
|
915
|
+
/>
|
|
916
|
+
<div className="inapp-scroll-div" />
|
|
917
|
+
</CapColumn>
|
|
918
|
+
<CapColumn span={10} className="inapp-preview-container">
|
|
919
|
+
{getPreviewSection()}
|
|
1358
920
|
</CapColumn>
|
|
1359
921
|
</CapRow>
|
|
1360
|
-
{
|
|
1361
|
-
|
|
1362
|
-
shouldUseHTMLEditor && (
|
|
922
|
+
<div className={`inapp-footer ${!isFullMode && `inapp-footer-lib`}`}>
|
|
923
|
+
{
|
|
1363
924
|
<>
|
|
1364
925
|
{hasAnyErrors(errorMessage) && (
|
|
1365
926
|
<ErrorInfoNote
|
|
@@ -1367,28 +928,24 @@ export const InApp = (props) => {
|
|
|
1367
928
|
currentTab={panes}
|
|
1368
929
|
/>
|
|
1369
930
|
)}
|
|
1370
|
-
<
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
);
|
|
1389
|
-
})()}
|
|
1390
|
-
</CapButton>
|
|
1391
|
-
{/* {!isFullMode && ( */}
|
|
931
|
+
<CapButton
|
|
932
|
+
onClick={isLiquidFlow ? liquidMiddleWare : onDoneCallback}
|
|
933
|
+
disabled={isDisableDone(panes)}
|
|
934
|
+
className="inapp-create-btn"
|
|
935
|
+
>
|
|
936
|
+
{isEditFlow ? (
|
|
937
|
+
isFullMode ? (
|
|
938
|
+
<FormattedMessage {...messages.update} />
|
|
939
|
+
) : (
|
|
940
|
+
<FormattedMessage {...globalMessages.done} />
|
|
941
|
+
)
|
|
942
|
+
) : isFullMode ? (
|
|
943
|
+
<FormattedMessage {...messages.create} />
|
|
944
|
+
) : (
|
|
945
|
+
<FormattedMessage {...globalMessages.done} />
|
|
946
|
+
)}
|
|
947
|
+
</CapButton>
|
|
948
|
+
{/* {!isFullMode && ( */}
|
|
1392
949
|
<CapButton
|
|
1393
950
|
onClick={handleTestAndPreview}
|
|
1394
951
|
className="inapp-test-preview-btn"
|
|
@@ -1397,10 +954,10 @@ export const InApp = (props) => {
|
|
|
1397
954
|
>
|
|
1398
955
|
<FormattedMessage {...creativesMessages.testAndPreview} />
|
|
1399
956
|
</CapButton>
|
|
1400
|
-
|
|
1401
|
-
</div>
|
|
957
|
+
{/* )} */}
|
|
1402
958
|
</>
|
|
1403
|
-
|
|
959
|
+
}
|
|
960
|
+
</div>
|
|
1404
961
|
<TestAndPreviewSlidebox
|
|
1405
962
|
show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
|
|
1406
963
|
onClose={handleCloseTestAndPreview}
|
|
@@ -1420,8 +977,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
1420
977
|
loadingTags: isLoadingMetaEntities(),
|
|
1421
978
|
injectedTags: setInjectedTags(),
|
|
1422
979
|
currentOrgDetails: selectCurrentOrgDetails(),
|
|
1423
|
-
fetchingLiquidValidation: selectLiquidStateDetails()
|
|
1424
|
-
getTemplateDetailsInProgress: makeSelectGetTemplateDetailsInProgress(),
|
|
980
|
+
fetchingLiquidValidation: selectLiquidStateDetails()
|
|
1425
981
|
});
|
|
1426
982
|
|
|
1427
983
|
const mapDispatchToProps = (dispatch) => ({
|