@capillarytech/creatives-library 8.0.304 → 8.0.305-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.
Files changed (60) hide show
  1. package/constants/unified.js +4 -1
  2. package/initialState.js +0 -2
  3. package/package.json +1 -1
  4. package/utils/common.js +12 -9
  5. package/utils/commonUtils.js +36 -93
  6. package/utils/tagValidations.js +83 -223
  7. package/utils/tests/commonUtil.test.js +147 -124
  8. package/utils/tests/tagValidations.test.js +441 -358
  9. package/v2Components/CapDeviceContent/index.js +10 -7
  10. package/v2Components/ErrorInfoNote/index.js +2 -5
  11. package/v2Components/FormBuilder/index.js +137 -203
  12. package/v2Components/FormBuilder/messages.js +0 -8
  13. package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
  15. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
  16. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
  17. package/v2Containers/BeePopupEditor/index.js +9 -2
  18. package/v2Containers/Cap/mockData.js +0 -14
  19. package/v2Containers/Cap/reducer.js +3 -55
  20. package/v2Containers/Cap/tests/reducer.test.js +0 -102
  21. package/v2Containers/CreativesContainer/SlideBoxContent.js +40 -4
  22. package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -5
  23. package/v2Containers/CreativesContainer/constants.js +6 -0
  24. package/v2Containers/CreativesContainer/index.js +47 -7
  25. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +69 -1
  26. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +121 -4
  27. package/v2Containers/Email/index.js +1 -5
  28. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +23 -70
  29. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +29 -137
  30. package/v2Containers/FTP/index.js +2 -51
  31. package/v2Containers/FTP/messages.js +0 -4
  32. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +110 -155
  33. package/v2Containers/InApp/index.js +216 -120
  34. package/v2Containers/InApp/tests/index.test.js +17 -6
  35. package/v2Containers/InApp/tests/mockData.js +1 -1
  36. package/v2Containers/InappAdvance/index.js +6 -110
  37. package/v2Containers/InappAdvance/tests/index.test.js +2 -0
  38. package/v2Containers/Line/Container/Text/index.js +0 -1
  39. package/v2Containers/MobilePush/Create/index.js +59 -19
  40. package/v2Containers/MobilePush/Edit/index.js +48 -20
  41. package/v2Containers/MobilePushNew/index.js +12 -32
  42. package/v2Containers/MobilepushWrapper/index.js +3 -1
  43. package/v2Containers/Rcs/index.js +12 -37
  44. package/v2Containers/Sms/Create/index.js +39 -3
  45. package/v2Containers/Sms/Create/messages.js +4 -0
  46. package/v2Containers/Sms/Edit/index.js +35 -3
  47. package/v2Containers/Sms/commonMethods.js +3 -6
  48. package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
  49. package/v2Containers/SmsTrai/Edit/index.js +11 -47
  50. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  51. package/v2Containers/SmsWrapper/index.js +2 -0
  52. package/v2Containers/TemplatesV2/index.js +28 -13
  53. package/v2Containers/Viber/index.js +0 -1
  54. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
  55. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  56. package/v2Containers/WebPush/Create/index.js +2 -2
  57. package/v2Containers/WebPush/Create/utils/validation.js +17 -8
  58. package/v2Containers/WebPush/Create/utils/validation.test.js +44 -24
  59. package/v2Containers/Whatsapp/index.js +9 -17
  60. package/v2Containers/Zalo/index.js +3 -11
@@ -14,6 +14,8 @@ import CapRow from "@capillarytech/cap-ui-library/CapRow";
14
14
  import CapColumn from "@capillarytech/cap-ui-library/CapColumn";
15
15
  import CapButton from "@capillarytech/cap-ui-library/CapButton";
16
16
  import CapNotification from "@capillarytech/cap-ui-library/CapNotification";
17
+ import CapTab from "@capillarytech/cap-ui-library/CapTab";
18
+ import CapInput from "@capillarytech/cap-ui-library/CapInput";
17
19
  import { makeSelectInApp, makeSelectAccount, makeSelectGetTemplateDetailsInProgress } from "./selectors";
18
20
  import * as globalActions from '../Cap/actions';
19
21
  import {
@@ -31,7 +33,6 @@ import creativesMessages from '../CreativesContainer/messages';
31
33
  import withCreatives from "../../hoc/withCreatives";
32
34
  import UnifiedPreview from "../../v2Components/CommonTestAndPreview/UnifiedPreview";
33
35
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
34
- import { validateTags } from "../../utils/tagValidations";
35
36
  import injectReducer from '../../utils/injectReducer';
36
37
  import v2InAppReducer from './reducer';
37
38
  import { v2InAppSagas } from './sagas';
@@ -51,19 +52,21 @@ import {
51
52
  LAYOUT_RADIO_OPTIONS,
52
53
  IOS_CAPITAL,
53
54
  } from "./constants";
54
- import { INAPP, SMS } from "../CreativesContainer/constants";
55
+ import { GENERIC, INAPP, SMS } from "../CreativesContainer/constants";
56
+ import { AI_CONTENT_BOT_DISABLED } from "../../constants/unified";
55
57
  import {
56
58
  ALL, TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
57
59
  } from "../Whatsapp/constants";
58
60
  import { getCdnUrl } from "../../utils/cdnTransformation";
59
61
  import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
60
62
  import { validateInAppContent } from "../../utils/commonUtils";
61
- import { hasLiquidSupportFeature } from "../../utils/common";
63
+ import { hasLiquidSupportFeature, hasNewEditorFlowInAppEnabled } from "../../utils/common";
62
64
  import formBuilderMessages from "../../v2Components/FormBuilder/messages";
63
65
  import HTMLEditor from "../../v2Components/HtmlEditor";
64
66
  import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
65
67
  import { INAPP_EDITOR_TYPES } from "../InAppWrapper/constants";
66
68
  import InappAdvanced from "../InappAdvance/index";
69
+ import CapDeviceContent from "../../v2Components/CapDeviceContent";
67
70
  import { ErrorInfoNote } from "../../v2Components/ErrorInfoNote";
68
71
 
69
72
  let editContent = {};
@@ -74,6 +77,7 @@ export const InApp = (props) => {
74
77
  actions,
75
78
  globalActions,
76
79
  isFullMode,
80
+ isLoyaltyModule,
77
81
  onCreateComplete,
78
82
  params,
79
83
  templateData = {},
@@ -878,7 +882,7 @@ export const InApp = (props) => {
878
882
  // Use 'html editor template' as title for HTML editor to differentiate from BEE editor
879
883
  title: isHTMLTemplate ? 'html editor template' : titleAndroid,
880
884
  message: androidMessage,
881
- bodyType: templateLayoutType,
885
+ bodyType: bodyTypeForBackend,
882
886
  expandableDetails: {
883
887
  style: androidExpandableStyle,
884
888
  message: androidMessage,
@@ -1079,39 +1083,55 @@ export const InApp = (props) => {
1079
1083
  };
1080
1084
 
1081
1085
  // Validation middleware for tag validation (both liquid and non-liquid flow)
1086
+ // Liquid validation (extractTags) runs only in library mode
1082
1087
  const validationMiddleWare = async () => {
1083
- // Set up callbacks for validation results
1088
+ // Normalize validator bucket keys to component state keys (ANDROID, IOS_CAPITAL, GENERIC)
1089
+ // so we don't merge e.g. 'android'/'ios'/'generic' with ANDROID/IOS/GENERIC and get duplicate/stale keys
1090
+ const normalizeErrorBuckets = (errors) => {
1091
+ const normalized = {
1092
+ [ANDROID]: [],
1093
+ [IOS_CAPITAL]: [],
1094
+ [GENERIC]: [],
1095
+ };
1096
+ if (!errors || typeof errors !== 'object') return normalized;
1097
+ const keyMap = {
1098
+ ANDROID,
1099
+ android: ANDROID,
1100
+ [IOS_CAPITAL]: IOS_CAPITAL,
1101
+ [IOS]: IOS_CAPITAL,
1102
+ ios: IOS_CAPITAL,
1103
+ GENERIC,
1104
+ generic: GENERIC,
1105
+ };
1106
+ for (const [key, value] of Object.entries(errors)) {
1107
+ const targetKey = keyMap[key];
1108
+ if (targetKey != null && Array.isArray(value)) {
1109
+ normalized[targetKey] = value;
1110
+ }
1111
+ }
1112
+ return normalized;
1113
+ };
1084
1114
  const onError = ({ standardErrors, liquidErrors }) => {
1085
1115
  setErrorMessage((prev) => ({
1086
- STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
1087
- LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
1116
+ STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...normalizeErrorBuckets(standardErrors) },
1117
+ LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...normalizeErrorBuckets(liquidErrors) },
1088
1118
  }));
1089
1119
  };
1090
1120
  const onSuccess = () => {
1091
- // Proceed with submission when validation is successful
1092
1121
  onDoneCallback();
1093
1122
  };
1094
1123
 
1095
- // Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
1096
- const hasTags = tags && tags.length > 0;
1097
-
1098
- // For liquid flow, use validateInAppContent
1099
- if (isLiquidFlow && hasTags) {
1100
- // Validate the INAPP content
1124
+ // Library mode: run extractTags validation (always, so we catch liquid errors even when tags not loaded)
1125
+ if (!isFullMode) {
1101
1126
  const payload = createPayload();
1102
1127
  validateInAppContent(payload, {
1103
- currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
1128
+ currentTab: panes === ANDROID ? 1 : 2,
1104
1129
  onError,
1105
1130
  onSuccess,
1106
1131
  getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
1107
1132
  formatMessage,
1108
1133
  messages: formBuilderMessages,
1109
- tagLookupMap: metaEntities?.tagLookupMap || {},
1110
- eventContextTags: metaEntities?.eventContextTags || [],
1111
- isLiquidFlow,
1112
- forwardedTags: {},
1113
1134
  skipTags: (tag) => {
1114
- // Skip certain tags if needed
1115
1135
  const skipRegexes = [
1116
1136
  /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
1117
1137
  /unsubscribe\(#[a-zA-Z\d]{6}\)/,
@@ -1119,103 +1139,15 @@ export const InApp = (props) => {
1119
1139
  /SURVEY.*\.TOKEN/,
1120
1140
  /^[A-Za-z].*\([a-zA-Z\d]*\)/,
1121
1141
  ];
1122
-
1123
1142
  return skipRegexes.some((regex) => regex.test(tag));
1124
1143
  },
1125
1144
  singleTab: getSingleTab(accountData),
1126
1145
  });
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
- isFullMode,
1159
- }) || {};
1160
-
1161
- if (validationResponse?.unsupportedTags?.length > 0) {
1162
- hasErrors = true;
1163
- newErrors.LIQUID_ERROR_MSG.ANDROID.push(
1164
- formatMessage(globalMessages.unsupportedTagsValidationError, {
1165
- unsupportedTags: validationResponse.unsupportedTags.join(", "),
1166
- })
1167
- );
1168
- }
1169
- if (validationResponse?.isBraceError) {
1170
- hasErrors = true;
1171
- newErrors.LIQUID_ERROR_MSG.ANDROID.push(
1172
- formatMessage(globalMessages.unbalanacedCurlyBraces)
1173
- );
1174
- }
1175
- }
1176
-
1177
- // Validate iOS content
1178
- if (iosSupported && iosContent && iosContent?.trim() !== '') {
1179
- const validationResponse = validateTags({
1180
- content: iosContent,
1181
- tagsParam: tags,
1182
- injectedTagsParams: injectedTags || {},
1183
- location,
1184
- tagModule: getDefaultTags,
1185
- eventContextTags: metaEntities?.eventContextTags || [],
1186
- isFullMode,
1187
- }) || {};
1188
-
1189
- if (validationResponse?.unsupportedTags?.length > 0) {
1190
- hasErrors = true;
1191
- newErrors.LIQUID_ERROR_MSG.IOS.push(
1192
- formatMessage(globalMessages.unsupportedTagsValidationError, {
1193
- unsupportedTags: validationResponse.unsupportedTags.join(", "),
1194
- })
1195
- );
1196
- }
1197
- if (validationResponse?.isBraceError) {
1198
- hasErrors = true;
1199
- newErrors.LIQUID_ERROR_MSG.IOS.push(
1200
- formatMessage(globalMessages.unbalanacedCurlyBraces)
1201
- );
1202
- }
1203
- }
1204
-
1205
- if (hasErrors) {
1206
- setErrorMessage(newErrors);
1207
- } else {
1208
- // No errors, proceed with submission
1209
- onSuccess();
1210
- }
1211
1146
  } else {
1212
- // No tags available, skip validation and proceed directly
1213
1147
  onSuccess();
1214
1148
  }
1215
1149
  };
1216
1150
 
1217
- const isLiquidFlow = hasLiquidSupportFeature();
1218
-
1219
1151
  // Check template data to determine editor type (for render decision)
1220
1152
  const templateDetails = isFullMode ? editData?.templateDetails : templateData;
1221
1153
  const versions = templateDetails?.versions || {};
@@ -1240,11 +1172,13 @@ export const InApp = (props) => {
1240
1172
  && !isBEEeditor
1241
1173
  && !isBeeFreeTemplate;
1242
1174
 
1175
+ const isNewEditorFlowEnabled = !isLoyaltyModule && hasNewEditorFlowInAppEnabled();
1176
+
1243
1177
  // Use state if available, otherwise fall back to direct data check
1244
- const shouldUseHTMLEditor = isHTMLTemplate || isHTMLTemplateFromData;
1178
+ const shouldUseHTMLEditor = isNewEditorFlowEnabled && (isHTMLTemplate || isHTMLTemplateFromData);
1245
1179
 
1246
1180
  // Only route to Bee editor if it's explicitly a Bee editor AND not an HTML template
1247
- const shouldUseBeeEditor = (isBEEeditor || isBeeFreeTemplate) && !shouldUseHTMLEditor;
1181
+ const shouldUseBeeEditor = isNewEditorFlowEnabled && (isBEEeditor || isBeeFreeTemplate) && !shouldUseHTMLEditor;
1248
1182
 
1249
1183
  // Early returns to avoid nested ternary
1250
1184
  if (isEditInApp && getTemplateDetailsInProgress) {
@@ -1280,10 +1214,157 @@ export const InApp = (props) => {
1280
1214
  );
1281
1215
  }
1282
1216
 
1217
+ // ── Old-flow helpers (used by CapDeviceContent when flag is disabled) ──────
1218
+ const isAiContentBotDisabled = currentOrgDetails?.accessibleFeatures?.includes(AI_CONTENT_BOT_DISABLED);
1219
+
1220
+ const templateDescErrorHandler = (value) => {
1221
+ const { unsupportedTags, isBraceError } = validateTags({
1222
+ content: value,
1223
+ tagsParam: tags,
1224
+ injectedTagsParams: injectedTags,
1225
+ location,
1226
+ tagModule: getDefaultTags,
1227
+ isFullMode,
1228
+ }) || {};
1229
+ if (unsupportedTags?.length > 0) {
1230
+ return formatMessage(globalMessages.unsupportedTagsValidationError, { unsupportedTags });
1231
+ }
1232
+ if (isBraceError) {
1233
+ return formatMessage(globalMessages.braceValidationError);
1234
+ }
1235
+ return '';
1236
+ };
1237
+
1238
+ const onCopyTitleAndContent = () => {
1239
+ if (panes === ANDROID) {
1240
+ setTitleAndroid(titleIos);
1241
+ setTemplateMessageAndroid(templateMessageIos);
1242
+ setInAppImageSrcAndroid(inAppImageSrcIos);
1243
+ } else {
1244
+ setTitleIos(titleAndroid);
1245
+ setTemplateMessageIos(templateMessageAndroid);
1246
+ setInAppImageSrcIos(inAppImageSrcAndroid);
1247
+ }
1248
+ };
1249
+
1250
+ const onTagSelect = (value, index) => {
1251
+ const tag = `{{${value}}}`;
1252
+ if (panes === ANDROID) {
1253
+ if (index === 0) setTitleAndroid((prev) => prev + tag);
1254
+ else setTemplateMessageAndroid((prev) => prev + tag);
1255
+ } else if (index === 0) {
1256
+ setTitleIos((prev) => prev + tag);
1257
+ } else {
1258
+ setTemplateMessageIos((prev) => prev + tag);
1259
+ }
1260
+ };
1261
+
1262
+ // Device support flags (same derivation as InappAdvance)
1263
+ const isAndroidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
1264
+ const isIosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
1265
+
1266
+ // CapDeviceContent tab panes (old flow)
1267
+ const DEVICE_PANES = [
1268
+ {
1269
+ content: (
1270
+ <CapDeviceContent
1271
+ panes={ANDROID}
1272
+ actions={actions}
1273
+ editData={editData}
1274
+ isFullMode={isFullMode}
1275
+ inAppImageSrc={inAppImageSrcAndroid}
1276
+ setInAppImageSrc={setInAppImageSrcAndroid}
1277
+ isEditFlow={isEditFlow}
1278
+ ctaData={ctaDataAndroid}
1279
+ setCtaData={setCtaDataAndroid}
1280
+ buttonType={buttonTypeAndroid}
1281
+ setButtonType={setButtonTypeAndroid}
1282
+ templateMediaType={templateMediaType}
1283
+ setTemplateMediaType={setTemplateMediaType}
1284
+ title={titleAndroid}
1285
+ setTitle={setTitleAndroid}
1286
+ templateMessageError={templateMessageErrorAndroid}
1287
+ templateMessage={templateMessageAndroid}
1288
+ setTemplateMessage={setTemplateMessageAndroid}
1289
+ setTemplateMessageError={setTemplateMessageErrorAndroid}
1290
+ addActionLink={addActionLinkAndroid}
1291
+ setAddActionLink={setAddActionLinkAndroid}
1292
+ deepLink={deepLink}
1293
+ deepLinkValue={deepLinkValueAndroid}
1294
+ setDeepLinkValue={setDeepLinkValueAndroid}
1295
+ onCopyTitleAndContent={onCopyTitleAndContent}
1296
+ isOtherDeviceSupported={isIosSupported}
1297
+ tags={tags}
1298
+ onTagSelect={onTagSelect}
1299
+ handleOnTagsContextChange={handleOnTagsContextChange}
1300
+ templateDescErrorHandler={templateDescErrorHandler}
1301
+ templateTitleError={templateTitleErrorAndroid}
1302
+ setTemplateTitleError={setTemplateTitleErrorAndroid}
1303
+ isAiContentBotDisabled={isAiContentBotDisabled}
1304
+ injectedTags={injectedTags}
1305
+ selectedOfferDetails={selectedOfferDetails}
1306
+ location={location}
1307
+ />
1308
+ ),
1309
+ tab: <FormattedMessage {...messages.Android} />,
1310
+ key: ANDROID,
1311
+ isSupported: isAndroidSupported,
1312
+ },
1313
+ {
1314
+ content: (
1315
+ <CapDeviceContent
1316
+ panes={IOS}
1317
+ actions={actions}
1318
+ editData={editData}
1319
+ isFullMode={isFullMode}
1320
+ inAppImageSrc={inAppImageSrcIos}
1321
+ setInAppImageSrc={setInAppImageSrcIos}
1322
+ isEditFlow={isEditFlow}
1323
+ ctaData={ctaDataIos}
1324
+ setCtaData={setCtaDataIos}
1325
+ buttonType={buttonTypeIos}
1326
+ setButtonType={setButtonTypeIos}
1327
+ templateMediaType={templateMediaType}
1328
+ setTemplateMediaType={setTemplateMediaType}
1329
+ title={titleIos}
1330
+ setTitle={setTitleIos}
1331
+ templateMessageError={templateMessageErrorIos}
1332
+ templateMessage={templateMessageIos}
1333
+ setTemplateMessage={setTemplateMessageIos}
1334
+ setTemplateMessageError={setTemplateMessageErrorIos}
1335
+ addActionLink={addActionLinkIos}
1336
+ setAddActionLink={setAddActionLinkIos}
1337
+ deepLink={deepLink}
1338
+ deepLinkValue={deepLinkValueIos}
1339
+ setDeepLinkValue={setDeepLinkValueIos}
1340
+ onCopyTitleAndContent={onCopyTitleAndContent}
1341
+ isOtherDeviceSupported={isAndroidSupported}
1342
+ tags={tags}
1343
+ onTagSelect={onTagSelect}
1344
+ handleOnTagsContextChange={handleOnTagsContextChange}
1345
+ templateDescErrorHandler={templateDescErrorHandler}
1346
+ templateTitleError={templateTitleErrorIos}
1347
+ setTemplateTitleError={setTemplateTitleErrorIos}
1348
+ isAiContentBotDisabled={isAiContentBotDisabled}
1349
+ injectedTags={injectedTags}
1350
+ selectedOfferDetails={selectedOfferDetails}
1351
+ location={location}
1352
+ />
1353
+ ),
1354
+ tab: <FormattedMessage {...messages.Ios} />,
1355
+ key: IOS,
1356
+ isSupported: isIosSupported,
1357
+ },
1358
+ ];
1359
+ // ─────────────────────────────────────────────────────────────────────────
1360
+
1361
+ // Calculate column span: HTML editor = 18, everything else = 24
1362
+ const editorColumnSpan = shouldUseHTMLEditor ? 18 : 24;
1363
+
1283
1364
  return (
1284
1365
  <CapSpin spinning={spin || fetchingLiquidValidation} tip={fetchingLiquidValidation ? <FormattedMessage {...formBuilderMessages.liquidSpinText} /> : ""}>
1285
1366
  <CapRow className="cap-inapp-creatives">
1286
- <CapColumn span={shouldUseHTMLEditor ? 18 : 24}>
1367
+ <CapColumn span={editorColumnSpan}>
1287
1368
  {/* Creative layout type */}
1288
1369
  {shouldUseHTMLEditor && (
1289
1370
  <>
@@ -1304,7 +1385,7 @@ export const InApp = (props) => {
1304
1385
  />
1305
1386
  </>
1306
1387
  )}
1307
- {shouldUseHTMLEditor ? (
1388
+ {shouldUseHTMLEditor && (
1308
1389
  <HTMLEditor
1309
1390
  key={`inapp-html-editor-v${htmlEditorContentVersion}`}
1310
1391
  variant={HTML_EDITOR_VARIANTS.INAPP}
@@ -1320,19 +1401,16 @@ export const InApp = (props) => {
1320
1401
  location={location}
1321
1402
  selectedOfferDetails={selectedOfferDetails}
1322
1403
  onTagSelect={() => {
1323
- // Tag insertion is handled by HTMLEditor's CodeEditorPane at cursor position
1324
- // Content updates will be propagated via onContentChange callback
1404
+ // Tag insertion handled by HTMLEditor's CodeEditorPane at cursor position
1325
1405
  }}
1326
- // Don't pass globalActions to prevent duplicate API calls
1327
- // HTMLEditor will use onContextChange instead
1328
1406
  onContextChange={handleOnTagsContextChange}
1329
- // Pass validation errors to HTMLEditor for display
1330
1407
  errors={errorMessage}
1331
1408
  isFullMode={isFullMode}
1332
1409
  data-test="inapp-html-editor"
1333
1410
  style={{ width: '138%' }}
1334
1411
  />
1335
- ) : (
1412
+ )}
1413
+ {!shouldUseHTMLEditor && isNewEditorFlowEnabled && (
1336
1414
  <InappAdvanced
1337
1415
  getFormData={getFormData}
1338
1416
  setIsLoadingContent={setIsLoadingContent}
@@ -1357,11 +1435,29 @@ export const InApp = (props) => {
1357
1435
  onCreateComplete={onCreateComplete}
1358
1436
  />
1359
1437
  )}
1438
+ {!shouldUseHTMLEditor && !isNewEditorFlowEnabled && (
1439
+ <>
1440
+ <CapInput
1441
+ label={<FormattedMessage {...messages.creativeName} />}
1442
+ onChange={({ target: { value } }) => setTempName(value)}
1443
+ value={tempName}
1444
+ labelPosition="top"
1445
+ size="default"
1446
+ />
1447
+ <CapTab
1448
+ panes={DEVICE_PANES.filter((devicePane) => devicePane?.isSupported === true)}
1449
+ onChange={(value) => setPanes(value)}
1450
+ activeKey={panes}
1451
+ defaultActiveKey={panes}
1452
+ className="inapp-template-device-tab"
1453
+ />
1454
+ </>
1455
+ )}
1360
1456
  </CapColumn>
1361
1457
  </CapRow>
1362
- {/* Footer with Done/Update button - Only show for HTML editor, bee editor has its own footer */}
1458
+ {/* Footer with Done/Update button - show for HTML editor and old CapDeviceContent flow */}
1363
1459
  {
1364
- shouldUseHTMLEditor && (
1460
+ (shouldUseHTMLEditor || !isNewEditorFlowEnabled) && (
1365
1461
  <>
1366
1462
  {hasAnyErrors(errorMessage) && (
1367
1463
  <ErrorInfoNote
@@ -21,18 +21,16 @@ 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';
24
25
 
25
26
  const mockActions = {
26
27
  getTemplateInfoById: jest.fn(),
27
28
  resetEditTemplate: jest.fn(),
28
- getTemplateDetails: jest.fn((id, callback) => {
29
+ getTemplateDetails: jest.fn((id, setSpin) => {
29
30
  // Simulate successful template details fetch to prevent loading state
30
31
  // The callback is setSpin function, call it with false to stop spinner
31
- if (callback && typeof callback === 'function') {
32
- // Use setTimeout to ensure it's called after render
33
- setTimeout(() => {
34
- callback(false);
35
- }, 0);
32
+ if (setSpin && typeof setSpin === 'function') {
33
+ setTimeout(() => setSpin(false), 0);
36
34
  }
37
35
  }),
38
36
  editTemplate: jest.fn(),
@@ -41,6 +39,9 @@ const mockActions = {
41
39
  };
42
40
  const mockGlobalActions = {
43
41
  fetchSchemaForEntity: jest.fn(),
42
+ getLiquidTags: jest.fn((content, callback) =>
43
+ callback({ askAiraResponse: { data: [], errors: [] }, isError: false }),
44
+ ),
44
45
  };
45
46
 
46
47
  jest.mock('../../../v2Containers/TagList/index.js', () => ({
@@ -66,7 +67,17 @@ const renderComponent = (props) =>
66
67
  );
67
68
 
68
69
  describe('Test activity inApp container', () => {
70
+ afterEach(() => {
71
+ jest.restoreAllMocks();
72
+ });
73
+
69
74
  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
+ });
70
81
  renderComponent({
71
82
  actions: mockActions,
72
83
  globalActions: mockGlobalActions,
@@ -894,5 +894,5 @@ export const deviceContentProps = {
894
894
  },
895
895
  ],
896
896
  tags: [],
897
+ isOtherDeviceSupported: true,
897
898
  };
898
-
@@ -50,9 +50,7 @@ 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";
54
53
  import { validateInAppContent } from "../../utils/commonUtils";
55
- import { hasLiquidSupportFeature } from "../../utils/common";
56
54
  import formBuilderMessages from "../../v2Components/FormBuilder/messages";
57
55
  import { getSingleTab, hasAnyErrors } from "../InApp/utils";
58
56
  import ErrorInfoNote from "../../v2Components/ErrorInfoNote";
@@ -817,24 +815,16 @@ export const InappAdvanced = (props) => {
817
815
  const latestHtmlValues = await saveAllBeeInstances();
818
816
  const payload = createPayload(latestHtmlValues);
819
817
 
820
- // Validate the INAPP content
821
- const isLiquidFlow = hasLiquidSupportFeature();
822
- // Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
823
- const hasTags = tags && tags.length > 0;
824
- if (isLiquidFlow && hasTags) {
825
- validateInAppContent(payload, {
826
- currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
818
+ // Liquid validation (extractTags) only in library mode
819
+ if (!isFullMode) {
820
+ await validateInAppContent(payload, {
821
+ currentTab: panes === ANDROID ? 1 : 2,
827
822
  onError,
828
823
  onSuccess,
829
824
  getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
830
825
  formatMessage,
831
826
  messages: formBuilderMessages,
832
- tagLookupMap: metaEntities?.tagLookupMap || {},
833
- eventContextTags: metaEntities?.eventContextTags || [],
834
- isLiquidFlow,
835
- forwardedTags: {},
836
827
  skipTags: (tag) => {
837
- // Skip certain tags if needed
838
828
  const skipRegexes = [
839
829
  /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
840
830
  /unsubscribe\(#[a-zA-Z\d]{6}\)/,
@@ -842,98 +832,12 @@ export const InappAdvanced = (props) => {
842
832
  /SURVEY.*\.TOKEN/,
843
833
  /^[A-Za-z].*\([a-zA-Z\d]*\)/,
844
834
  ];
845
-
846
835
  return skipRegexes.some((regex) => regex.test(tag));
847
836
  },
848
837
  singleTab: getSingleTab(accountData),
849
838
  });
850
- } else if (hasTags) {
851
- // For non-liquid flow, validate tags using validateTags (only if tags are available)
852
- const androidContent = latestHtmlValues?.android || (androidBeeHtml?.value || (typeof androidBeeHtml === 'string' ? androidBeeHtml : ''));
853
- const iosContent = latestHtmlValues?.ios || (iosBeeHtml?.value || (typeof iosBeeHtml === 'string' ? iosBeeHtml : ''));
854
-
855
- const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
856
- const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
857
-
858
- let hasErrors = false;
859
- const newErrors = {
860
- STANDARD_ERROR_MSG: {
861
- ANDROID: [],
862
- IOS: [],
863
- GENERIC: [],
864
- },
865
- LIQUID_ERROR_MSG: {
866
- ANDROID: [],
867
- IOS: [],
868
- GENERIC: [],
869
- },
870
- };
871
-
872
- // Validate Android content
873
- if (androidSupported && androidContent && androidContent?.trim() !== '') {
874
- const validationResponse = validateTags({
875
- content: androidContent,
876
- tagsParam: tags,
877
- injectedTagsParams: injectedTags || {},
878
- location,
879
- tagModule: getDefaultTags,
880
- eventContextTags: metaEntities?.eventContextTags || [],
881
- isFullMode,
882
- }) || {};
883
-
884
- if (validationResponse?.unsupportedTags?.length > 0) {
885
- hasErrors = true;
886
- newErrors.LIQUID_ERROR_MSG.ANDROID.push(
887
- formatMessage(globalMessages.unsupportedTagsValidationError, {
888
- unsupportedTags: validationResponse.unsupportedTags.join(", "),
889
- })
890
- );
891
- }
892
- if (validationResponse?.isBraceError) {
893
- hasErrors = true;
894
- newErrors.LIQUID_ERROR_MSG.ANDROID.push(
895
- formatMessage(globalMessages.unbalanacedCurlyBraces)
896
- );
897
- }
898
- }
899
-
900
- // Validate iOS content
901
- if (iosSupported && iosContent && iosContent?.trim() !== '') {
902
- const validationResponse = validateTags({
903
- content: iosContent,
904
- tagsParam: tags,
905
- injectedTagsParams: injectedTags || {},
906
- location,
907
- tagModule: getDefaultTags,
908
- eventContextTags: metaEntities?.eventContextTags || [],
909
- isFullMode,
910
- }) || {};
911
-
912
- if (validationResponse?.unsupportedTags?.length > 0) {
913
- hasErrors = true;
914
- newErrors.LIQUID_ERROR_MSG.IOS.push(
915
- formatMessage(globalMessages.unsupportedTagsValidationError, {
916
- unsupportedTags: validationResponse.unsupportedTags.join(", "),
917
- })
918
- );
919
- }
920
- if (validationResponse?.isBraceError) {
921
- hasErrors = true;
922
- newErrors.LIQUID_ERROR_MSG.IOS.push(
923
- formatMessage(globalMessages.unbalanacedCurlyBraces)
924
- );
925
- }
926
- }
927
-
928
- if (hasErrors) {
929
- setErrorMessage(newErrors);
930
- } else {
931
- // No errors, proceed with submission
932
- onSuccess();
933
- }
934
839
  } else {
935
- // No tags available, skip validation and proceed directly
936
- onSuccess();
840
+ await onSuccess();
937
841
  }
938
842
  };
939
843
 
@@ -1038,15 +942,7 @@ export const InappAdvanced = (props) => {
1038
942
  )}
1039
943
  <CapButton
1040
944
  onClick={async () => {
1041
- const isLiquidFlow = hasLiquidSupportFeature();
1042
- const hasTags = tags && tags?.length > 0;
1043
- if (isLiquidFlow || hasTags) {
1044
- // Use validation middleware for tag validation
1045
- await liquidMiddleWare();
1046
- } else {
1047
- // No validation needed, proceed directly
1048
- await onDoneCallback();
1049
- }
945
+ await liquidMiddleWare();
1050
946
  }}
1051
947
  disabled={isDisableDone()}
1052
948
  className="inapp-create-btn"