@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.
Files changed (55) hide show
  1. package/constants/unified.js +1 -3
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +85 -4
  6. package/utils/tagValidations.js +223 -83
  7. package/utils/tests/commonUtil.test.js +124 -147
  8. package/utils/tests/tagValidations.test.js +358 -441
  9. package/v2Components/ErrorInfoNote/index.js +5 -2
  10. package/v2Components/FormBuilder/index.js +203 -137
  11. package/v2Components/FormBuilder/messages.js +8 -0
  12. package/v2Components/HtmlEditor/HTMLEditor.js +11 -2
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  15. package/v2Components/HtmlEditor/_htmlEditor.scss +6 -1
  16. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +10 -10
  17. package/v2Components/HtmlEditor/components/DeviceToggle/FLOW_AND_CLICK_BEHAVIOUR.md +70 -0
  18. package/v2Components/HtmlEditor/hooks/useInAppContent.js +15 -8
  19. package/v2Containers/Cap/mockData.js +14 -0
  20. package/v2Containers/Cap/reducer.js +55 -3
  21. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  22. package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -5
  23. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  24. package/v2Containers/CreativesContainer/index.js +10 -30
  25. package/v2Containers/Email/index.js +5 -1
  26. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
  27. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +137 -29
  28. package/v2Containers/FTP/index.js +51 -2
  29. package/v2Containers/FTP/messages.js +4 -0
  30. package/v2Containers/InApp/index.js +139 -22
  31. package/v2Containers/InApp/tests/index.test.js +6 -17
  32. package/v2Containers/InappAdvance/index.js +118 -8
  33. package/v2Containers/InappAdvance/tests/index.test.js +2 -3
  34. package/v2Containers/Line/Container/Text/index.js +1 -0
  35. package/v2Containers/MobilePush/Create/index.js +19 -42
  36. package/v2Containers/MobilePush/Edit/index.js +19 -42
  37. package/v2Containers/MobilePushNew/index.js +32 -12
  38. package/v2Containers/MobilepushWrapper/index.js +1 -3
  39. package/v2Containers/Rcs/index.js +37 -12
  40. package/v2Containers/Sms/Create/index.js +3 -39
  41. package/v2Containers/Sms/Create/messages.js +0 -4
  42. package/v2Containers/Sms/Edit/index.js +3 -35
  43. package/v2Containers/Sms/commonMethods.js +6 -3
  44. package/v2Containers/SmsTrai/Edit/index.js +47 -11
  45. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  46. package/v2Containers/SmsWrapper/index.js +0 -2
  47. package/v2Containers/Viber/index.js +1 -0
  48. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  49. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  50. package/v2Containers/WebPush/Create/index.js +2 -2
  51. package/v2Containers/WebPush/Create/utils/validation.js +2 -17
  52. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -59
  53. package/v2Containers/Whatsapp/index.js +17 -9
  54. package/v2Containers/Zalo/index.js +11 -3
  55. 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: templateMessageAndroid,
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: templateMessageIos,
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 callback when in edit mode + full mode to show template name header
357
- if (showTemplateName && name) {
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
- // The onChange callback from useInAppContent passes the full deviceContent object
1005
- // But we only want to update the state for the device that actually changed
1006
- // Use the second parameter (changedDevice) if provided, otherwise update both
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
- if (changedDevice) {
1026
- // Only update the device that actually changed
1027
- if (changedDevice.toUpperCase() === ANDROID && deviceContent?.android !== undefined) {
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 && deviceContent?.ios !== undefined) {
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 (for backward compatibility)
1034
- // Only update if value actually changed to avoid unnecessary re-renders
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 (deviceContent?.ios !== undefined) {
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, IOS, setErrorMessage]);
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
- // Library mode: run extractTags validation (always, so we catch liquid errors even when tags not loaded)
1093
- if (!isFullMode) {
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, setSpin) => {
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 (setSpin && typeof setSpin === 'function') {
33
- setTimeout(() => setSpin(false), 0);
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
- if (editContent && !isEmpty(editContent)) {
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: params?.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
- // Liquid validation (extractTags) only in library mode
819
- if (!isFullMode) {
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
- if (isEditFlow) {
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: params && params?.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
- await liquidMiddleWare();
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: { id: 'test-id' },
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',
@@ -137,6 +137,7 @@ export const LineText = ({
137
137
  const { valid, isBraceError } = validateTags({
138
138
  content: value,
139
139
  tagsParam: tags,
140
+ injectedTagsParams: injectedTags,
140
141
  location,
141
142
  tagModule: 'outbound',
142
143
  isFullMode,
@@ -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, validateMobilePushContent } from '../../../utils/commonUtils';
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
- // Library mode: on Done click (transition to isGetFormData), run extractTags only when no existing validation errors (braces, personalization, etc.)
99
- if (nextProps.isGetFormData && !this.props.isGetFormData && !nextProps.isFullMode) {
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, schema} = this.state;
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] || {}).includes(true);
718
- const isIosInvalid = Object.values(errorData[1] || {}).includes(true);
719
- const isIosTabVisible = get(schema, 'containers[0].panes[1].isSupported', true) !== false;
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
- } else if (isIosInvalid && isIosTabVisible) {
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,