@capillarytech/creatives-library 8.0.292-alpha.0 → 8.0.292-alpha.10
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/constants/unified.js +1 -3
- package/initialState.js +2 -0
- package/package.json +1 -1
- package/utils/common.js +8 -5
- package/utils/commonUtils.js +85 -4
- package/utils/tagValidations.js +223 -83
- package/utils/tests/commonUtil.test.js +124 -147
- package/utils/tests/tagValidations.test.js +358 -441
- package/v2Components/ErrorInfoNote/index.js +5 -2
- package/v2Components/FormBuilder/index.js +203 -137
- package/v2Components/FormBuilder/messages.js +8 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +11 -2
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +6 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +10 -10
- package/v2Components/HtmlEditor/components/DeviceToggle/FLOW_AND_CLICK_BEHAVIOUR.md +70 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +15 -8
- package/v2Containers/Cap/mockData.js +14 -0
- package/v2Containers/Cap/reducer.js +55 -3
- package/v2Containers/Cap/tests/reducer.test.js +102 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -5
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
- package/v2Containers/CreativesContainer/index.js +10 -30
- package/v2Containers/Email/index.js +5 -1
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +137 -29
- package/v2Containers/FTP/index.js +51 -2
- package/v2Containers/FTP/messages.js +4 -0
- package/v2Containers/InApp/index.js +139 -22
- package/v2Containers/InApp/tests/index.test.js +6 -17
- package/v2Containers/InappAdvance/index.js +118 -8
- package/v2Containers/InappAdvance/tests/index.test.js +2 -3
- package/v2Containers/Line/Container/Text/index.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +19 -42
- package/v2Containers/MobilePush/Edit/index.js +19 -42
- package/v2Containers/MobilePushNew/index.js +32 -12
- package/v2Containers/MobilepushWrapper/index.js +1 -3
- package/v2Containers/Rcs/index.js +37 -12
- package/v2Containers/Sms/Create/index.js +3 -39
- package/v2Containers/Sms/Create/messages.js +0 -4
- package/v2Containers/Sms/Edit/index.js +3 -35
- package/v2Containers/Sms/commonMethods.js +6 -3
- package/v2Containers/SmsTrai/Edit/index.js +47 -11
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/SmsWrapper/index.js +0 -2
- package/v2Containers/Viber/index.js +1 -0
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
- package/v2Containers/WebPush/Create/index.js +2 -2
- package/v2Containers/WebPush/Create/utils/validation.js +2 -17
- package/v2Containers/WebPush/Create/utils/validation.test.js +24 -59
- package/v2Containers/Whatsapp/index.js +17 -9
- package/v2Containers/Zalo/index.js +11 -3
- package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
|
@@ -31,6 +31,7 @@ import creativesMessages from '../CreativesContainer/messages';
|
|
|
31
31
|
import withCreatives from "../../hoc/withCreatives";
|
|
32
32
|
import UnifiedPreview from "../../v2Components/CommonTestAndPreview/UnifiedPreview";
|
|
33
33
|
import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
|
|
34
|
+
import { validateTags } from "../../utils/tagValidations";
|
|
34
35
|
import injectReducer from '../../utils/injectReducer';
|
|
35
36
|
import v2InAppReducer from './reducer';
|
|
36
37
|
import { v2InAppSagas } from './sagas';
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
import { getCdnUrl } from "../../utils/cdnTransformation";
|
|
58
59
|
import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
|
|
59
60
|
import { validateInAppContent } from "../../utils/commonUtils";
|
|
61
|
+
import { hasLiquidSupportFeature } from "../../utils/common";
|
|
60
62
|
import formBuilderMessages from "../../v2Components/FormBuilder/messages";
|
|
61
63
|
import HTMLEditor from "../../v2Components/HtmlEditor";
|
|
62
64
|
import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
|
|
@@ -175,6 +177,10 @@ export const InApp = (props) => {
|
|
|
175
177
|
// Transformation to payload structure happens in prepareTestMessagePayload
|
|
176
178
|
// Reference: Based on getPreviewSection() function (lines 490-530) which prepares content for TemplatePreview
|
|
177
179
|
const getTemplateContent = useCallback(() => {
|
|
180
|
+
// For HTML template, use HTML editor content so preview stays in sync with typing
|
|
181
|
+
const androidMsg = isHTMLTemplate ? (htmlContentAndroid ?? '') : templateMessageAndroid;
|
|
182
|
+
const iosMsg = isHTMLTemplate ? (htmlContentIos ?? '') : templateMessageIos;
|
|
183
|
+
|
|
178
184
|
// Prepare Android content
|
|
179
185
|
const androidMediaPreview = {};
|
|
180
186
|
if (templateMediaType === INAPP_MEDIA_TYPES.IMAGE) {
|
|
@@ -184,7 +190,7 @@ export const InApp = (props) => {
|
|
|
184
190
|
const androidContent = {
|
|
185
191
|
mediaPreview: androidMediaPreview,
|
|
186
192
|
templateTitle: titleAndroid,
|
|
187
|
-
templateMsg:
|
|
193
|
+
templateMsg: androidMsg,
|
|
188
194
|
...(isBtnTypeCtaAndroid && {
|
|
189
195
|
ctaData: ctaDataAndroid,
|
|
190
196
|
}),
|
|
@@ -202,7 +208,7 @@ export const InApp = (props) => {
|
|
|
202
208
|
const iosContent = {
|
|
203
209
|
mediaPreview: iosMediaPreview,
|
|
204
210
|
templateTitle: titleIos,
|
|
205
|
-
templateMsg:
|
|
211
|
+
templateMsg: iosMsg,
|
|
206
212
|
...(isBtnTypeCTaIos && {
|
|
207
213
|
ctaData: ctaDataIos,
|
|
208
214
|
}),
|
|
@@ -233,6 +239,9 @@ export const InApp = (props) => {
|
|
|
233
239
|
templateLayoutType,
|
|
234
240
|
deepLinkValueAndroid,
|
|
235
241
|
deepLinkValueIos,
|
|
242
|
+
isHTMLTemplate,
|
|
243
|
+
htmlContentAndroid,
|
|
244
|
+
htmlContentIos,
|
|
236
245
|
]);
|
|
237
246
|
|
|
238
247
|
// Handle Test and Preview button click
|
|
@@ -353,10 +362,10 @@ export const InApp = (props) => {
|
|
|
353
362
|
setTempName(name);
|
|
354
363
|
setTemplateDate(createdAt);
|
|
355
364
|
setTemplateLayoutType(editContent?.ANDROID?.bodyType);
|
|
356
|
-
// Call showTemplateName
|
|
357
|
-
if (showTemplateName
|
|
365
|
+
// Call showTemplateName when in edit mode so the header with "Edit name" shows (same as Email)
|
|
366
|
+
if (showTemplateName) {
|
|
358
367
|
showTemplateName({
|
|
359
|
-
formData: { 'template-name': name },
|
|
368
|
+
formData: { 'template-name': name || '' },
|
|
360
369
|
onFormDataChange: (updatedFormData) => {
|
|
361
370
|
const newName = updatedFormData?.['template-name'] || '';
|
|
362
371
|
setTempName(newName);
|
|
@@ -1001,12 +1010,11 @@ export const InApp = (props) => {
|
|
|
1001
1010
|
|
|
1002
1011
|
// Handle HTML content changes from HTMLEditor
|
|
1003
1012
|
const handleHtmlContentChange = useCallback((deviceContent, changedDevice) => {
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1013
|
+
// When "keep content same" is on, useInAppContent passes full deviceContent with both
|
|
1014
|
+
// android and ios set to the same value. We must update both states so preview shows
|
|
1015
|
+
// the synced content for each device. Otherwise only update the device that changed.
|
|
1007
1016
|
|
|
1008
1017
|
// Clear validation errors when content changes (similar to Bee Editor)
|
|
1009
|
-
// This ensures Done button re-enables after user fixes errors
|
|
1010
1018
|
if (changedDevice) {
|
|
1011
1019
|
setErrorMessage((prev) => ({
|
|
1012
1020
|
STANDARD_ERROR_MSG: {
|
|
@@ -1022,30 +1030,41 @@ export const InApp = (props) => {
|
|
|
1022
1030
|
}));
|
|
1023
1031
|
}
|
|
1024
1032
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1033
|
+
const hasAndroid = deviceContent?.android !== undefined;
|
|
1034
|
+
const hasIos = deviceContent?.ios !== undefined;
|
|
1035
|
+
const isSyncUpdate = hasAndroid && hasIos;
|
|
1036
|
+
|
|
1037
|
+
if (isSyncUpdate) {
|
|
1038
|
+
// Sync mode: update both so preview shows same content for Android and iOS
|
|
1039
|
+
if (hasAndroid) {
|
|
1040
|
+
setHtmlContentAndroid(deviceContent.android || '');
|
|
1041
|
+
}
|
|
1042
|
+
if (hasIos) {
|
|
1043
|
+
setHtmlContentIos(deviceContent.ios || '');
|
|
1044
|
+
}
|
|
1045
|
+
} else if (changedDevice) {
|
|
1046
|
+
// Only one device changed
|
|
1047
|
+
if (changedDevice.toUpperCase() === ANDROID && hasAndroid) {
|
|
1028
1048
|
setHtmlContentAndroid(deviceContent.android || '');
|
|
1029
|
-
} else if (changedDevice.toUpperCase() === IOS_CAPITAL &&
|
|
1049
|
+
} else if (changedDevice.toUpperCase() === IOS_CAPITAL && hasIos) {
|
|
1030
1050
|
setHtmlContentIos(deviceContent.ios || '');
|
|
1031
1051
|
}
|
|
1032
1052
|
} else {
|
|
1033
|
-
// Fallback: update both if changedDevice not provided
|
|
1034
|
-
|
|
1035
|
-
if (deviceContent?.android !== undefined) {
|
|
1053
|
+
// Fallback: update both if changedDevice not provided
|
|
1054
|
+
if (hasAndroid) {
|
|
1036
1055
|
setHtmlContentAndroid((prev) => {
|
|
1037
1056
|
const newValue = deviceContent.android || '';
|
|
1038
1057
|
return prev !== newValue ? newValue : prev;
|
|
1039
1058
|
});
|
|
1040
1059
|
}
|
|
1041
|
-
if (
|
|
1060
|
+
if (hasIos) {
|
|
1042
1061
|
setHtmlContentIos((prev) => {
|
|
1043
1062
|
const newValue = deviceContent.ios || '';
|
|
1044
1063
|
return prev !== newValue ? newValue : prev;
|
|
1045
1064
|
});
|
|
1046
1065
|
}
|
|
1047
1066
|
}
|
|
1048
|
-
}, [ANDROID,
|
|
1067
|
+
}, [ANDROID, IOS_CAPITAL, setErrorMessage]);
|
|
1049
1068
|
|
|
1050
1069
|
// Handle HTML save from HTMLEditor
|
|
1051
1070
|
const handleHtmlSave = useCallback((deviceContent) => {
|
|
@@ -1077,8 +1096,8 @@ export const InApp = (props) => {
|
|
|
1077
1096
|
};
|
|
1078
1097
|
|
|
1079
1098
|
// Validation middleware for tag validation (both liquid and non-liquid flow)
|
|
1080
|
-
// Liquid validation (extractTags) runs only in library mode
|
|
1081
1099
|
const validationMiddleWare = async () => {
|
|
1100
|
+
// Set up callbacks for validation results
|
|
1082
1101
|
const onError = ({ standardErrors, liquidErrors }) => {
|
|
1083
1102
|
setErrorMessage((prev) => ({
|
|
1084
1103
|
STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
|
|
@@ -1086,20 +1105,30 @@ export const InApp = (props) => {
|
|
|
1086
1105
|
}));
|
|
1087
1106
|
};
|
|
1088
1107
|
const onSuccess = () => {
|
|
1108
|
+
// Proceed with submission when validation is successful
|
|
1089
1109
|
onDoneCallback();
|
|
1090
1110
|
};
|
|
1091
1111
|
|
|
1092
|
-
//
|
|
1093
|
-
|
|
1112
|
+
// Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
|
|
1113
|
+
const hasTags = tags && tags.length > 0;
|
|
1114
|
+
|
|
1115
|
+
// For liquid flow, use validateInAppContent
|
|
1116
|
+
if (isLiquidFlow && hasTags) {
|
|
1117
|
+
// Validate the INAPP content
|
|
1094
1118
|
const payload = createPayload();
|
|
1095
1119
|
validateInAppContent(payload, {
|
|
1096
|
-
currentTab: panes === ANDROID ? 1 : 2,
|
|
1120
|
+
currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
|
|
1097
1121
|
onError,
|
|
1098
1122
|
onSuccess,
|
|
1099
1123
|
getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
|
|
1100
1124
|
formatMessage,
|
|
1101
1125
|
messages: formBuilderMessages,
|
|
1126
|
+
tagLookupMap: metaEntities?.tagLookupMap || {},
|
|
1127
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1128
|
+
isLiquidFlow,
|
|
1129
|
+
forwardedTags: {},
|
|
1102
1130
|
skipTags: (tag) => {
|
|
1131
|
+
// Skip certain tags if needed
|
|
1103
1132
|
const skipRegexes = [
|
|
1104
1133
|
/dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
|
|
1105
1134
|
/unsubscribe\(#[a-zA-Z\d]{6}\)/,
|
|
@@ -1107,15 +1136,103 @@ export const InApp = (props) => {
|
|
|
1107
1136
|
/SURVEY.*\.TOKEN/,
|
|
1108
1137
|
/^[A-Za-z].*\([a-zA-Z\d]*\)/,
|
|
1109
1138
|
];
|
|
1139
|
+
|
|
1110
1140
|
return skipRegexes.some((regex) => regex.test(tag));
|
|
1111
1141
|
},
|
|
1112
1142
|
singleTab: getSingleTab(accountData),
|
|
1113
1143
|
});
|
|
1144
|
+
} else if (hasTags) {
|
|
1145
|
+
// For non-liquid flow, validate tags using validateTags (only if tags are available)
|
|
1146
|
+
const androidContent = htmlContentAndroid || '';
|
|
1147
|
+
const iosContent = htmlContentIos || '';
|
|
1148
|
+
|
|
1149
|
+
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
1150
|
+
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
1151
|
+
|
|
1152
|
+
let hasErrors = false;
|
|
1153
|
+
const newErrors = {
|
|
1154
|
+
STANDARD_ERROR_MSG: {
|
|
1155
|
+
ANDROID: [],
|
|
1156
|
+
IOS: [],
|
|
1157
|
+
GENERIC: [],
|
|
1158
|
+
},
|
|
1159
|
+
LIQUID_ERROR_MSG: {
|
|
1160
|
+
ANDROID: [],
|
|
1161
|
+
IOS: [],
|
|
1162
|
+
GENERIC: [],
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
// Validate Android content
|
|
1167
|
+
if (androidSupported && androidContent && androidContent?.trim() !== '') {
|
|
1168
|
+
const validationResponse = validateTags({
|
|
1169
|
+
content: androidContent,
|
|
1170
|
+
tagsParam: tags,
|
|
1171
|
+
injectedTagsParams: injectedTags || {},
|
|
1172
|
+
location,
|
|
1173
|
+
tagModule: getDefaultTags,
|
|
1174
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1175
|
+
isFullMode,
|
|
1176
|
+
}) || {};
|
|
1177
|
+
|
|
1178
|
+
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
1179
|
+
hasErrors = true;
|
|
1180
|
+
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
1181
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1182
|
+
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
1183
|
+
})
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
if (validationResponse?.isBraceError) {
|
|
1187
|
+
hasErrors = true;
|
|
1188
|
+
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
1189
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Validate iOS content
|
|
1195
|
+
if (iosSupported && iosContent && iosContent?.trim() !== '') {
|
|
1196
|
+
const validationResponse = validateTags({
|
|
1197
|
+
content: iosContent,
|
|
1198
|
+
tagsParam: tags,
|
|
1199
|
+
injectedTagsParams: injectedTags || {},
|
|
1200
|
+
location,
|
|
1201
|
+
tagModule: getDefaultTags,
|
|
1202
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1203
|
+
isFullMode,
|
|
1204
|
+
}) || {};
|
|
1205
|
+
|
|
1206
|
+
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
1207
|
+
hasErrors = true;
|
|
1208
|
+
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
1209
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1210
|
+
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
1211
|
+
})
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
if (validationResponse?.isBraceError) {
|
|
1215
|
+
hasErrors = true;
|
|
1216
|
+
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
1217
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (hasErrors) {
|
|
1223
|
+
setErrorMessage(newErrors);
|
|
1224
|
+
} else {
|
|
1225
|
+
// No errors, proceed with submission
|
|
1226
|
+
onSuccess();
|
|
1227
|
+
}
|
|
1114
1228
|
} else {
|
|
1229
|
+
// No tags available, skip validation and proceed directly
|
|
1115
1230
|
onSuccess();
|
|
1116
1231
|
}
|
|
1117
1232
|
};
|
|
1118
1233
|
|
|
1234
|
+
const isLiquidFlow = hasLiquidSupportFeature();
|
|
1235
|
+
|
|
1119
1236
|
// Check template data to determine editor type (for render decision)
|
|
1120
1237
|
const templateDetails = isFullMode ? editData?.templateDetails : templateData;
|
|
1121
1238
|
const versions = templateDetails?.versions || {};
|
|
@@ -21,16 +21,18 @@ import { getCtaObject } from '../utils';
|
|
|
21
21
|
jest.mock('redux-auth-wrapper/history4/redirect', () => ({
|
|
22
22
|
connectedRouterRedirect: jest.fn((config) => (Component) => Component),
|
|
23
23
|
}));
|
|
24
|
-
import * as commonUtils from '../../../utils/commonUtils';
|
|
25
24
|
|
|
26
25
|
const mockActions = {
|
|
27
26
|
getTemplateInfoById: jest.fn(),
|
|
28
27
|
resetEditTemplate: jest.fn(),
|
|
29
|
-
getTemplateDetails: jest.fn((id,
|
|
28
|
+
getTemplateDetails: jest.fn((id, callback) => {
|
|
30
29
|
// Simulate successful template details fetch to prevent loading state
|
|
31
30
|
// The callback is setSpin function, call it with false to stop spinner
|
|
32
|
-
if (
|
|
33
|
-
setTimeout
|
|
31
|
+
if (callback && typeof callback === 'function') {
|
|
32
|
+
// Use setTimeout to ensure it's called after render
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
callback(false);
|
|
35
|
+
}, 0);
|
|
34
36
|
}
|
|
35
37
|
}),
|
|
36
38
|
editTemplate: jest.fn(),
|
|
@@ -39,9 +41,6 @@ const mockActions = {
|
|
|
39
41
|
};
|
|
40
42
|
const mockGlobalActions = {
|
|
41
43
|
fetchSchemaForEntity: jest.fn(),
|
|
42
|
-
getLiquidTags: jest.fn((content, callback) =>
|
|
43
|
-
callback({ askAiraResponse: { data: [], errors: [] }, isError: false }),
|
|
44
|
-
),
|
|
45
44
|
};
|
|
46
45
|
|
|
47
46
|
jest.mock('../../../v2Containers/TagList/index.js', () => ({
|
|
@@ -67,17 +66,7 @@ const renderComponent = (props) =>
|
|
|
67
66
|
);
|
|
68
67
|
|
|
69
68
|
describe('Test activity inApp container', () => {
|
|
70
|
-
afterEach(() => {
|
|
71
|
-
jest.restoreAllMocks();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
69
|
it('test case for inApp template update flow', async () => {
|
|
75
|
-
jest
|
|
76
|
-
.spyOn(commonUtils, 'validateInAppContent')
|
|
77
|
-
.mockImplementation((payload, options) => {
|
|
78
|
-
options.onSuccess();
|
|
79
|
-
return Promise.resolve(true);
|
|
80
|
-
});
|
|
81
70
|
renderComponent({
|
|
82
71
|
actions: mockActions,
|
|
83
72
|
globalActions: mockGlobalActions,
|
|
@@ -50,7 +50,9 @@ import injectReducer from '../../utils/injectReducer';
|
|
|
50
50
|
import v2InAppReducer from '../InApp/reducer';
|
|
51
51
|
import { v2InAppSagas } from '../InApp/sagas';
|
|
52
52
|
import injectSaga from '../../utils/injectSaga';
|
|
53
|
+
import { validateTags } from "../../utils/tagValidations";
|
|
53
54
|
import { validateInAppContent } from "../../utils/commonUtils";
|
|
55
|
+
import { hasLiquidSupportFeature } from "../../utils/common";
|
|
54
56
|
import formBuilderMessages from "../../v2Components/FormBuilder/messages";
|
|
55
57
|
import { getSingleTab, hasAnyErrors } from "../InApp/utils";
|
|
56
58
|
import ErrorInfoNote from "../../v2Components/ErrorInfoNote";
|
|
@@ -115,6 +117,9 @@ export const InappAdvanced = (props) => {
|
|
|
115
117
|
},
|
|
116
118
|
});
|
|
117
119
|
|
|
120
|
+
// Template id for edit: use params (route), templateData (library), or fetched templateDetails
|
|
121
|
+
const templateId = params?.id || get(templateData, '_id') || get(editData, 'templateDetails._id');
|
|
122
|
+
|
|
118
123
|
//fetching bee popup builder token
|
|
119
124
|
useEffect(() => {
|
|
120
125
|
actions.getBeePopupBuilderToken();
|
|
@@ -141,7 +146,9 @@ export const InappAdvanced = (props) => {
|
|
|
141
146
|
} = isFullMode ? editData?.templateDetails || {} : templateData || {};
|
|
142
147
|
editContent = get(versions, `base.content`, {});
|
|
143
148
|
|
|
144
|
-
|
|
149
|
+
// Set edit flow when we have loaded content OR when we have a template id (edit page)
|
|
150
|
+
const hasTemplateId = params?.id || get(templateData, '_id') || get(editData, 'templateDetails._id');
|
|
151
|
+
if ((editContent && !isEmpty(editContent)) || (hasTemplateId && (editData?.templateDetails || templateData))) {
|
|
145
152
|
setEditFlow(true);
|
|
146
153
|
if (setTemplateName && name) {
|
|
147
154
|
setTemplateName(name);
|
|
@@ -664,7 +671,7 @@ export const InappAdvanced = (props) => {
|
|
|
664
671
|
actions.editTemplate(
|
|
665
672
|
{
|
|
666
673
|
...payload,
|
|
667
|
-
_id:
|
|
674
|
+
_id: templateId,
|
|
668
675
|
},
|
|
669
676
|
(resp, errorMsg) => {
|
|
670
677
|
actionCallback({ resp, errorMessage: errorMsg });
|
|
@@ -815,16 +822,24 @@ export const InappAdvanced = (props) => {
|
|
|
815
822
|
const latestHtmlValues = await saveAllBeeInstances();
|
|
816
823
|
const payload = createPayload(latestHtmlValues);
|
|
817
824
|
|
|
818
|
-
//
|
|
819
|
-
|
|
825
|
+
// Validate the INAPP content
|
|
826
|
+
const isLiquidFlow = hasLiquidSupportFeature();
|
|
827
|
+
// Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
|
|
828
|
+
const hasTags = tags && tags.length > 0;
|
|
829
|
+
if (isLiquidFlow && hasTags) {
|
|
820
830
|
validateInAppContent(payload, {
|
|
821
|
-
currentTab: panes === ANDROID ? 1 : 2,
|
|
831
|
+
currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
|
|
822
832
|
onError,
|
|
823
833
|
onSuccess,
|
|
824
834
|
getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
|
|
825
835
|
formatMessage,
|
|
826
836
|
messages: formBuilderMessages,
|
|
837
|
+
tagLookupMap: metaEntities?.tagLookupMap || {},
|
|
838
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
839
|
+
isLiquidFlow,
|
|
840
|
+
forwardedTags: {},
|
|
827
841
|
skipTags: (tag) => {
|
|
842
|
+
// Skip certain tags if needed
|
|
828
843
|
const skipRegexes = [
|
|
829
844
|
/dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
|
|
830
845
|
/unsubscribe\(#[a-zA-Z\d]{6}\)/,
|
|
@@ -832,18 +847,105 @@ export const InappAdvanced = (props) => {
|
|
|
832
847
|
/SURVEY.*\.TOKEN/,
|
|
833
848
|
/^[A-Za-z].*\([a-zA-Z\d]*\)/,
|
|
834
849
|
];
|
|
850
|
+
|
|
835
851
|
return skipRegexes.some((regex) => regex.test(tag));
|
|
836
852
|
},
|
|
837
853
|
singleTab: getSingleTab(accountData),
|
|
838
854
|
});
|
|
855
|
+
} else if (hasTags) {
|
|
856
|
+
// For non-liquid flow, validate tags using validateTags (only if tags are available)
|
|
857
|
+
const androidContent = latestHtmlValues?.android || (androidBeeHtml?.value || (typeof androidBeeHtml === 'string' ? androidBeeHtml : ''));
|
|
858
|
+
const iosContent = latestHtmlValues?.ios || (iosBeeHtml?.value || (typeof iosBeeHtml === 'string' ? iosBeeHtml : ''));
|
|
859
|
+
|
|
860
|
+
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
861
|
+
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
862
|
+
|
|
863
|
+
let hasErrors = false;
|
|
864
|
+
const newErrors = {
|
|
865
|
+
STANDARD_ERROR_MSG: {
|
|
866
|
+
ANDROID: [],
|
|
867
|
+
IOS: [],
|
|
868
|
+
GENERIC: [],
|
|
869
|
+
},
|
|
870
|
+
LIQUID_ERROR_MSG: {
|
|
871
|
+
ANDROID: [],
|
|
872
|
+
IOS: [],
|
|
873
|
+
GENERIC: [],
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// Validate Android content
|
|
878
|
+
if (androidSupported && androidContent && androidContent?.trim() !== '') {
|
|
879
|
+
const validationResponse = validateTags({
|
|
880
|
+
content: androidContent,
|
|
881
|
+
tagsParam: tags,
|
|
882
|
+
injectedTagsParams: injectedTags || {},
|
|
883
|
+
location,
|
|
884
|
+
tagModule: getDefaultTags,
|
|
885
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
886
|
+
isFullMode,
|
|
887
|
+
}) || {};
|
|
888
|
+
|
|
889
|
+
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
890
|
+
hasErrors = true;
|
|
891
|
+
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
892
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
893
|
+
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
894
|
+
})
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (validationResponse?.isBraceError) {
|
|
898
|
+
hasErrors = true;
|
|
899
|
+
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
900
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Validate iOS content
|
|
906
|
+
if (iosSupported && iosContent && iosContent?.trim() !== '') {
|
|
907
|
+
const validationResponse = validateTags({
|
|
908
|
+
content: iosContent,
|
|
909
|
+
tagsParam: tags,
|
|
910
|
+
injectedTagsParams: injectedTags || {},
|
|
911
|
+
location,
|
|
912
|
+
tagModule: getDefaultTags,
|
|
913
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
914
|
+
isFullMode,
|
|
915
|
+
}) || {};
|
|
916
|
+
|
|
917
|
+
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
918
|
+
hasErrors = true;
|
|
919
|
+
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
920
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
921
|
+
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
922
|
+
})
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
if (validationResponse?.isBraceError) {
|
|
926
|
+
hasErrors = true;
|
|
927
|
+
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
928
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (hasErrors) {
|
|
934
|
+
setErrorMessage(newErrors);
|
|
935
|
+
} else {
|
|
936
|
+
// No errors, proceed with submission
|
|
937
|
+
onSuccess();
|
|
938
|
+
}
|
|
839
939
|
} else {
|
|
940
|
+
// No tags available, skip validation and proceed directly
|
|
840
941
|
onSuccess();
|
|
841
942
|
}
|
|
842
943
|
};
|
|
843
944
|
|
|
844
945
|
const onDoneCallback = async () => {
|
|
845
946
|
if (isFullMode) {
|
|
846
|
-
|
|
947
|
+
// Edit when we have template id (from route/templateData/fetched details) or isEditFlow set from content
|
|
948
|
+
if (templateId || isEditFlow) {
|
|
847
949
|
await onEditInApp();
|
|
848
950
|
return;
|
|
849
951
|
}
|
|
@@ -854,7 +956,7 @@ export const InappAdvanced = (props) => {
|
|
|
854
956
|
const latestHtmlValues = await saveAllBeeInstances();
|
|
855
957
|
getFormData({
|
|
856
958
|
value: createPayload(latestHtmlValues),
|
|
857
|
-
_id:
|
|
959
|
+
_id: templateId,
|
|
858
960
|
validity: true,
|
|
859
961
|
type: INAPP,
|
|
860
962
|
});
|
|
@@ -942,7 +1044,15 @@ export const InappAdvanced = (props) => {
|
|
|
942
1044
|
)}
|
|
943
1045
|
<CapButton
|
|
944
1046
|
onClick={async () => {
|
|
945
|
-
|
|
1047
|
+
const isLiquidFlow = hasLiquidSupportFeature();
|
|
1048
|
+
const hasTags = tags && tags?.length > 0;
|
|
1049
|
+
if (isLiquidFlow || hasTags) {
|
|
1050
|
+
// Use validation middleware for tag validation
|
|
1051
|
+
await liquidMiddleWare();
|
|
1052
|
+
} else {
|
|
1053
|
+
// No validation needed, proceed directly
|
|
1054
|
+
await onDoneCallback();
|
|
1055
|
+
}
|
|
946
1056
|
}}
|
|
947
1057
|
disabled={isDisableDone()}
|
|
948
1058
|
className="inapp-create-btn"
|
|
@@ -61,7 +61,6 @@ describe('InappAdvanced Component', () => {
|
|
|
61
61
|
|
|
62
62
|
const mockGlobalActions = {
|
|
63
63
|
fetchSchemaForEntity: jest.fn(),
|
|
64
|
-
getLiquidTags: jest.fn((content, callback) => callback({ askAiraResponse: { data: [] }, isError: false })),
|
|
65
64
|
};
|
|
66
65
|
|
|
67
66
|
defaultProps = {
|
|
@@ -72,7 +71,7 @@ describe('InappAdvanced Component', () => {
|
|
|
72
71
|
globalActions: mockGlobalActions,
|
|
73
72
|
isFullMode: true,
|
|
74
73
|
onCreateComplete: jest.fn(),
|
|
75
|
-
params: {
|
|
74
|
+
params: {}, // No id so create mode (edit flow not triggered)
|
|
76
75
|
templateData: {},
|
|
77
76
|
editData: {},
|
|
78
77
|
accountData: {
|
|
@@ -299,6 +298,7 @@ describe('InappAdvanced Component', () => {
|
|
|
299
298
|
renderComponent({
|
|
300
299
|
...defaultProps,
|
|
301
300
|
isFullMode: false,
|
|
301
|
+
params: { id: 'test-id' },
|
|
302
302
|
getFormData: mockGetFormData,
|
|
303
303
|
});
|
|
304
304
|
|
|
@@ -410,7 +410,6 @@ describe('InappAdvanced Component', () => {
|
|
|
410
410
|
actions: mockActions,
|
|
411
411
|
globalActions: {
|
|
412
412
|
fetchSchemaForEntity: jest.fn(),
|
|
413
|
-
getLiquidTags: jest.fn((content, callback) => callback({ askAiraResponse: { data: [] }, isError: false })),
|
|
414
413
|
},
|
|
415
414
|
location: {
|
|
416
415
|
pathname: '/inapp/create',
|
|
@@ -38,8 +38,7 @@ import v2MobilePushCreateReducer from './reducer';
|
|
|
38
38
|
import { v2MobilePushWatchDuplicateTemplateSaga } from './sagas';
|
|
39
39
|
import { EXTERNAL_LINK_LOWERCASE } from './constants';
|
|
40
40
|
import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
|
|
41
|
-
import { checkForPersonalizationTokens
|
|
42
|
-
import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
|
|
41
|
+
import { checkForPersonalizationTokens } from '../../../utils/commonUtils';
|
|
43
42
|
|
|
44
43
|
const PrefixWrapper = styled.div`
|
|
45
44
|
margin-right: 16px;
|
|
@@ -95,39 +94,8 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
95
94
|
this.props.globalActions.fetchSchemaForEntity(query);
|
|
96
95
|
};
|
|
97
96
|
componentWillReceiveProps = (nextProps) => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// If form already has validation errors (braces, personalization tags, etc.), do not call extractTags; just fail
|
|
101
|
-
if (!this.state.isFormValid && nextProps.onValidationFail) {
|
|
102
|
-
nextProps.onValidationFail();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter && nextProps.onValidationFail) {
|
|
106
|
-
const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
|
|
107
|
-
validateMobilePushContent(formDataArr, {
|
|
108
|
-
currentTab: this.state.currentTab,
|
|
109
|
-
getLiquidTags: nextProps.getLiquidTags,
|
|
110
|
-
formatMessage: this.props.intl.formatMessage,
|
|
111
|
-
messages: formBuilderMessages,
|
|
112
|
-
onError: (err) => {
|
|
113
|
-
const { standardErrors = [], liquidErrors = [] } = err;
|
|
114
|
-
// _validatePlatformSpecificContent passes { standardErrors: { ANDROID, IOS, generic }, liquidErrors: { ... } }; footer expects arrays
|
|
115
|
-
const toArray = (v) => (Array.isArray(v) ? v : (v && typeof v === 'object' ? [].concat(...Object.values(v)) : []));
|
|
116
|
-
const STANDARD_ERROR_MSG = toArray(standardErrors);
|
|
117
|
-
const LIQUID_ERROR_MSG = toArray(liquidErrors);
|
|
118
|
-
nextProps.showLiquidErrorInFooter(
|
|
119
|
-
{ STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
|
|
120
|
-
this.state.currentTab
|
|
121
|
-
);
|
|
122
|
-
nextProps.onValidationFail();
|
|
123
|
-
},
|
|
124
|
-
onSuccess: () => {
|
|
125
|
-
nextProps.getFormLibraryData(this.getFormData());
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
} else {
|
|
129
|
-
nextProps.getFormLibraryData(this.getFormData());
|
|
130
|
-
}
|
|
97
|
+
if (nextProps.isGetFormData && !this.props.isFullMode) {
|
|
98
|
+
nextProps.getFormLibraryData(this.getFormData());
|
|
131
99
|
} else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
|
|
132
100
|
this.startValidation();
|
|
133
101
|
}
|
|
@@ -710,21 +678,31 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
710
678
|
}
|
|
711
679
|
showError = () => {
|
|
712
680
|
const {intl} = this.props;
|
|
713
|
-
const {errorData
|
|
681
|
+
const {errorData} = this.state;
|
|
714
682
|
const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
|
|
715
683
|
if (!isEmpty(this.state.formData) && !this.state.isFormValid) {
|
|
716
684
|
let tab = this.state.currentTab;
|
|
717
|
-
const isAndroidInvalid = Object.values(errorData[0]
|
|
718
|
-
const isIosInvalid = Object.values(errorData[1]
|
|
719
|
-
|
|
685
|
+
const isAndroidInvalid = Object.values(errorData[0]).includes(true);
|
|
686
|
+
const isIosInvalid = Object.values(errorData[1]).includes(true);
|
|
687
|
+
let isTagErrorExist = false;
|
|
720
688
|
if (isAndroidInvalid) {
|
|
721
689
|
tab = 1;
|
|
722
690
|
errorMessage.description = intl.formatMessage(messages.invalidAndroidMessage);
|
|
723
|
-
|
|
691
|
+
const invalidTags = errorData[0]['invalid-tags'];
|
|
692
|
+
if (!isEmpty(invalidTags)) {
|
|
693
|
+
isTagErrorExist = true;
|
|
694
|
+
errorMessage.description = `${intl.formatMessage(messages.invalidAndroidMessage)} ${intl.formatMessage(messages.invalidTags)}: ${invalidTags.join(',')} `;
|
|
695
|
+
}
|
|
696
|
+
} else if (isIosInvalid) {
|
|
724
697
|
tab = 2;
|
|
725
698
|
errorMessage.description = intl.formatMessage(messages.invalidIosMessage);
|
|
699
|
+
const invalidTags = errorData[1]['invalid-tags'];
|
|
700
|
+
if (!isEmpty(invalidTags)) {
|
|
701
|
+
isTagErrorExist = true;
|
|
702
|
+
errorMessage.description = `${intl.formatMessage(messages.invalidIosMessage)} ${intl.formatMessage(messages.invalidTags)}: ${invalidTags.join(',')} `;
|
|
703
|
+
}
|
|
726
704
|
}
|
|
727
|
-
if (tab !== this.state.currentTab) {
|
|
705
|
+
if (tab !== this.state.currentTab || isTagErrorExist) {
|
|
728
706
|
CapNotification.error(errorMessage);
|
|
729
707
|
}
|
|
730
708
|
}
|
|
@@ -2051,7 +2029,6 @@ Create.propTypes = {
|
|
|
2051
2029
|
onPreviewContentClicked: PropTypes.func,
|
|
2052
2030
|
onTestContentClicked: PropTypes.func,
|
|
2053
2031
|
eventContextTags: PropTypes.array,
|
|
2054
|
-
getLiquidTags: PropTypes.func,
|
|
2055
2032
|
showLiquidErrorInFooter: PropTypes.func,
|
|
2056
2033
|
showTestAndPreviewSlidebox: PropTypes.bool,
|
|
2057
2034
|
handleTestAndPreview: PropTypes.func,
|