@capillarytech/creatives-library 8.0.307 → 8.0.308

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 (54) hide show
  1. package/constants/unified.js +1 -5
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +93 -36
  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 +5 -0
  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/components/CodeEditorPane/index.js +2 -1
  16. package/v2Containers/Cap/mockData.js +14 -0
  17. package/v2Containers/Cap/reducer.js +55 -3
  18. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  19. package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -5
  20. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  21. package/v2Containers/CreativesContainer/constants.js +0 -6
  22. package/v2Containers/CreativesContainer/index.js +7 -47
  23. package/v2Containers/Email/index.js +5 -1
  24. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
  25. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +120 -20
  26. package/v2Containers/FTP/index.js +51 -2
  27. package/v2Containers/FTP/messages.js +4 -0
  28. package/v2Containers/InApp/index.js +122 -35
  29. package/v2Containers/InApp/tests/index.test.js +6 -17
  30. package/v2Containers/InappAdvance/index.js +112 -4
  31. package/v2Containers/InappAdvance/tests/index.test.js +0 -2
  32. package/v2Containers/Line/Container/Text/index.js +1 -0
  33. package/v2Containers/MobilePush/Create/index.js +19 -59
  34. package/v2Containers/MobilePush/Edit/index.js +20 -48
  35. package/v2Containers/MobilePushNew/index.js +32 -12
  36. package/v2Containers/MobilepushWrapper/index.js +1 -3
  37. package/v2Containers/Rcs/index.js +37 -12
  38. package/v2Containers/Sms/Create/index.js +3 -39
  39. package/v2Containers/Sms/Create/messages.js +0 -4
  40. package/v2Containers/Sms/Edit/index.js +3 -35
  41. package/v2Containers/Sms/commonMethods.js +6 -3
  42. package/v2Containers/SmsTrai/Edit/index.js +47 -11
  43. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  44. package/v2Containers/SmsWrapper/index.js +0 -2
  45. package/v2Containers/TemplatesV2/index.js +13 -28
  46. package/v2Containers/Viber/index.js +1 -0
  47. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  48. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  49. package/v2Containers/WebPush/Create/index.js +2 -2
  50. package/v2Containers/WebPush/Create/utils/validation.js +8 -17
  51. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -44
  52. package/v2Containers/Whatsapp/index.js +17 -9
  53. package/v2Containers/Zalo/index.js +11 -3
  54. package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
@@ -33,6 +33,7 @@ import creativesMessages from '../CreativesContainer/messages';
33
33
  import withCreatives from "../../hoc/withCreatives";
34
34
  import UnifiedPreview from "../../v2Components/CommonTestAndPreview/UnifiedPreview";
35
35
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
36
+ import { validateTags } from "../../utils/tagValidations";
36
37
  import injectReducer from '../../utils/injectReducer';
37
38
  import v2InAppReducer from './reducer';
38
39
  import { v2InAppSagas } from './sagas';
@@ -52,7 +53,7 @@ import {
52
53
  LAYOUT_RADIO_OPTIONS,
53
54
  IOS_CAPITAL,
54
55
  } from "./constants";
55
- import { GENERIC, INAPP, SMS } from "../CreativesContainer/constants";
56
+ import { INAPP, SMS } from "../CreativesContainer/constants";
56
57
  import { AI_CONTENT_BOT_DISABLED } from "../../constants/unified";
57
58
  import {
58
59
  ALL, TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
@@ -60,8 +61,7 @@ import {
60
61
  import { getCdnUrl } from "../../utils/cdnTransformation";
61
62
  import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
62
63
  import { validateInAppContent } from "../../utils/commonUtils";
63
- import { validateTags } from "../../utils/tagValidations";
64
- import { hasNewEditorFlowInAppEnabled } from "../../utils/common";
64
+ import { hasLiquidSupportFeature, hasNewEditorFlowInAppEnabled } from "../../utils/common";
65
65
  import formBuilderMessages from "../../v2Components/FormBuilder/messages";
66
66
  import HTMLEditor from "../../v2Components/HtmlEditor";
67
67
  import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
@@ -1084,55 +1084,39 @@ export const InApp = (props) => {
1084
1084
  };
1085
1085
 
1086
1086
  // Validation middleware for tag validation (both liquid and non-liquid flow)
1087
- // Liquid validation (extractTags) runs only in library mode
1088
1087
  const validationMiddleWare = async () => {
1089
- // Normalize validator bucket keys to component state keys (ANDROID, IOS_CAPITAL, GENERIC)
1090
- // so we don't merge e.g. 'android'/'ios'/'generic' with ANDROID/IOS/GENERIC and get duplicate/stale keys
1091
- const normalizeErrorBuckets = (errors) => {
1092
- const normalized = {
1093
- [ANDROID]: [],
1094
- [IOS_CAPITAL]: [],
1095
- [GENERIC]: [],
1096
- };
1097
- if (!errors || typeof errors !== 'object') return normalized;
1098
- const keyMap = {
1099
- ANDROID,
1100
- android: ANDROID,
1101
- [IOS_CAPITAL]: IOS_CAPITAL,
1102
- [IOS]: IOS_CAPITAL,
1103
- ios: IOS_CAPITAL,
1104
- GENERIC,
1105
- generic: GENERIC,
1106
- };
1107
- for (const [key, value] of Object.entries(errors)) {
1108
- const targetKey = keyMap[key];
1109
- if (targetKey != null && Array.isArray(value)) {
1110
- normalized[targetKey] = value;
1111
- }
1112
- }
1113
- return normalized;
1114
- };
1088
+ // Set up callbacks for validation results
1115
1089
  const onError = ({ standardErrors, liquidErrors }) => {
1116
1090
  setErrorMessage((prev) => ({
1117
- STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...normalizeErrorBuckets(standardErrors) },
1118
- LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...normalizeErrorBuckets(liquidErrors) },
1091
+ STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
1092
+ LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
1119
1093
  }));
1120
1094
  };
1121
1095
  const onSuccess = () => {
1096
+ // Proceed with submission when validation is successful
1122
1097
  onDoneCallback();
1123
1098
  };
1124
1099
 
1125
- // Library mode: run extractTags validation (always, so we catch liquid errors even when tags not loaded)
1126
- if (!isFullMode) {
1100
+ // Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
1101
+ const hasTags = tags && tags.length > 0;
1102
+
1103
+ // For liquid flow, use validateInAppContent
1104
+ if (isLiquidFlow && hasTags) {
1105
+ // Validate the INAPP content
1127
1106
  const payload = createPayload();
1128
1107
  validateInAppContent(payload, {
1129
- currentTab: panes === ANDROID ? 1 : 2,
1108
+ currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
1130
1109
  onError,
1131
1110
  onSuccess,
1132
1111
  getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
1133
1112
  formatMessage,
1134
1113
  messages: formBuilderMessages,
1114
+ tagLookupMap: metaEntities?.tagLookupMap || {},
1115
+ eventContextTags: metaEntities?.eventContextTags || [],
1116
+ isLiquidFlow,
1117
+ forwardedTags: {},
1135
1118
  skipTags: (tag) => {
1119
+ // Skip certain tags if needed
1136
1120
  const skipRegexes = [
1137
1121
  /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
1138
1122
  /unsubscribe\(#[a-zA-Z\d]{6}\)/,
@@ -1140,15 +1124,103 @@ export const InApp = (props) => {
1140
1124
  /SURVEY.*\.TOKEN/,
1141
1125
  /^[A-Za-z].*\([a-zA-Z\d]*\)/,
1142
1126
  ];
1127
+
1143
1128
  return skipRegexes.some((regex) => regex.test(tag));
1144
1129
  },
1145
1130
  singleTab: getSingleTab(accountData),
1146
1131
  });
1132
+ } else if (hasTags) {
1133
+ // For non-liquid flow, validate tags using validateTags (only if tags are available)
1134
+ const androidContent = htmlContentAndroid || '';
1135
+ const iosContent = htmlContentIos || '';
1136
+
1137
+ const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
1138
+ const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
1139
+
1140
+ let hasErrors = false;
1141
+ const newErrors = {
1142
+ STANDARD_ERROR_MSG: {
1143
+ ANDROID: [],
1144
+ IOS: [],
1145
+ GENERIC: [],
1146
+ },
1147
+ LIQUID_ERROR_MSG: {
1148
+ ANDROID: [],
1149
+ IOS: [],
1150
+ GENERIC: [],
1151
+ },
1152
+ };
1153
+
1154
+ // Validate Android content
1155
+ if (androidSupported && androidContent && androidContent?.trim() !== '') {
1156
+ const validationResponse = validateTags({
1157
+ content: androidContent,
1158
+ tagsParam: tags,
1159
+ injectedTagsParams: injectedTags || {},
1160
+ location,
1161
+ tagModule: getDefaultTags,
1162
+ eventContextTags: metaEntities?.eventContextTags || [],
1163
+ isFullMode,
1164
+ }) || {};
1165
+
1166
+ if (validationResponse?.unsupportedTags?.length > 0) {
1167
+ hasErrors = true;
1168
+ newErrors.LIQUID_ERROR_MSG.ANDROID.push(
1169
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1170
+ unsupportedTags: validationResponse.unsupportedTags.join(", "),
1171
+ })
1172
+ );
1173
+ }
1174
+ if (validationResponse?.isBraceError) {
1175
+ hasErrors = true;
1176
+ newErrors.LIQUID_ERROR_MSG.ANDROID.push(
1177
+ formatMessage(globalMessages.unbalanacedCurlyBraces)
1178
+ );
1179
+ }
1180
+ }
1181
+
1182
+ // Validate iOS content
1183
+ if (iosSupported && iosContent && iosContent?.trim() !== '') {
1184
+ const validationResponse = validateTags({
1185
+ content: iosContent,
1186
+ tagsParam: tags,
1187
+ injectedTagsParams: injectedTags || {},
1188
+ location,
1189
+ tagModule: getDefaultTags,
1190
+ eventContextTags: metaEntities?.eventContextTags || [],
1191
+ isFullMode,
1192
+ }) || {};
1193
+
1194
+ if (validationResponse?.unsupportedTags?.length > 0) {
1195
+ hasErrors = true;
1196
+ newErrors.LIQUID_ERROR_MSG.IOS.push(
1197
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1198
+ unsupportedTags: validationResponse.unsupportedTags.join(", "),
1199
+ })
1200
+ );
1201
+ }
1202
+ if (validationResponse?.isBraceError) {
1203
+ hasErrors = true;
1204
+ newErrors.LIQUID_ERROR_MSG.IOS.push(
1205
+ formatMessage(globalMessages.unbalanacedCurlyBraces)
1206
+ );
1207
+ }
1208
+ }
1209
+
1210
+ if (hasErrors) {
1211
+ setErrorMessage(newErrors);
1212
+ } else {
1213
+ // No errors, proceed with submission
1214
+ onSuccess();
1215
+ }
1147
1216
  } else {
1217
+ // No tags available, skip validation and proceed directly
1148
1218
  onSuccess();
1149
1219
  }
1150
1220
  };
1151
1221
 
1222
+ const isLiquidFlow = hasLiquidSupportFeature();
1223
+
1152
1224
  // Check template data to determine editor type (for render decision)
1153
1225
  const templateDetails = isFullMode ? editData?.templateDetails : templateData;
1154
1226
  const versions = templateDetails?.versions || {};
@@ -1445,6 +1517,21 @@ export const InApp = (props) => {
1445
1517
  labelPosition="top"
1446
1518
  size="default"
1447
1519
  />
1520
+ <CapRow>
1521
+ <CapHeading type="h4">
1522
+ <FormattedMessage {...messages.creativeLayout} />
1523
+ </CapHeading>
1524
+ <CapHeading type="h6" className="inapp-creative-layout-desc">
1525
+ <FormattedMessage {...messages.creativeLayoutDesc} />
1526
+ </CapHeading>
1527
+ </CapRow>
1528
+ <CapRadioGroup
1529
+ id="inapp-layout-radio"
1530
+ options={LAYOUT_RADIO_OPTIONS}
1531
+ value={templateLayoutType}
1532
+ onChange={onTemplateLayoutTypeChange}
1533
+ className="inapp-layout-radio"
1534
+ />
1448
1535
  <CapTab
1449
1536
  panes={DEVICE_PANES.filter((devicePane) => devicePane?.isSupported === true)}
1450
1537
  onChange={(value) => setPanes(value)}
@@ -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";
@@ -816,18 +818,30 @@ export const InappAdvanced = (props) => {
816
818
  const payload = createPayload(latestHtmlValues);
817
819
 
818
820
  // Validate the INAPP content
821
+ const isLiquidFlow = hasLiquidSupportFeature();
819
822
  // Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
820
823
  const hasTags = tags && tags.length > 0;
821
824
 
822
- if (!isFullMode || hasTags) {
823
- await validateInAppContent(payload, {
825
+ // When liquid is enabled and isFullMode is true, skip liquid validation and proceed directly
826
+ if (isLiquidFlow && isFullMode) {
827
+ onSuccess();
828
+ return;
829
+ }
830
+
831
+ if (isLiquidFlow && hasTags) {
832
+ validateInAppContent(payload, {
824
833
  currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
825
834
  onError,
826
835
  onSuccess,
827
836
  getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
828
837
  formatMessage,
829
838
  messages: formBuilderMessages,
839
+ tagLookupMap: metaEntities?.tagLookupMap || {},
840
+ eventContextTags: metaEntities?.eventContextTags || [],
841
+ isLiquidFlow,
842
+ forwardedTags: {},
830
843
  skipTags: (tag) => {
844
+ // Skip certain tags if needed
831
845
  const skipRegexes = [
832
846
  /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
833
847
  /unsubscribe\(#[a-zA-Z\d]{6}\)/,
@@ -835,12 +849,98 @@ export const InappAdvanced = (props) => {
835
849
  /SURVEY.*\.TOKEN/,
836
850
  /^[A-Za-z].*\([a-zA-Z\d]*\)/,
837
851
  ];
852
+
838
853
  return skipRegexes.some((regex) => regex.test(tag));
839
854
  },
840
855
  singleTab: getSingleTab(accountData),
841
856
  });
857
+ } else if (hasTags) {
858
+ // For non-liquid flow, validate tags using validateTags (only if tags are available)
859
+ const androidContent = latestHtmlValues?.android || (androidBeeHtml?.value || (typeof androidBeeHtml === 'string' ? androidBeeHtml : ''));
860
+ const iosContent = latestHtmlValues?.ios || (iosBeeHtml?.value || (typeof iosBeeHtml === 'string' ? iosBeeHtml : ''));
861
+
862
+ const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
863
+ const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
864
+
865
+ let hasErrors = false;
866
+ const newErrors = {
867
+ STANDARD_ERROR_MSG: {
868
+ ANDROID: [],
869
+ IOS: [],
870
+ GENERIC: [],
871
+ },
872
+ LIQUID_ERROR_MSG: {
873
+ ANDROID: [],
874
+ IOS: [],
875
+ GENERIC: [],
876
+ },
877
+ };
878
+
879
+ // Validate Android content
880
+ if (androidSupported && androidContent && androidContent?.trim() !== '') {
881
+ const validationResponse = validateTags({
882
+ content: androidContent,
883
+ tagsParam: tags,
884
+ injectedTagsParams: injectedTags || {},
885
+ location,
886
+ tagModule: getDefaultTags,
887
+ eventContextTags: metaEntities?.eventContextTags || [],
888
+ isFullMode,
889
+ }) || {};
890
+
891
+ if (validationResponse?.unsupportedTags?.length > 0) {
892
+ hasErrors = true;
893
+ newErrors.LIQUID_ERROR_MSG.ANDROID.push(
894
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
895
+ unsupportedTags: validationResponse.unsupportedTags.join(", "),
896
+ })
897
+ );
898
+ }
899
+ if (validationResponse?.isBraceError) {
900
+ hasErrors = true;
901
+ newErrors.LIQUID_ERROR_MSG.ANDROID.push(
902
+ formatMessage(globalMessages.unbalanacedCurlyBraces)
903
+ );
904
+ }
905
+ }
906
+
907
+ // Validate iOS content
908
+ if (iosSupported && iosContent && iosContent?.trim() !== '') {
909
+ const validationResponse = validateTags({
910
+ content: iosContent,
911
+ tagsParam: tags,
912
+ injectedTagsParams: injectedTags || {},
913
+ location,
914
+ tagModule: getDefaultTags,
915
+ eventContextTags: metaEntities?.eventContextTags || [],
916
+ isFullMode,
917
+ }) || {};
918
+
919
+ if (validationResponse?.unsupportedTags?.length > 0) {
920
+ hasErrors = true;
921
+ newErrors.LIQUID_ERROR_MSG.IOS.push(
922
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
923
+ unsupportedTags: validationResponse.unsupportedTags.join(", "),
924
+ })
925
+ );
926
+ }
927
+ if (validationResponse?.isBraceError) {
928
+ hasErrors = true;
929
+ newErrors.LIQUID_ERROR_MSG.IOS.push(
930
+ formatMessage(globalMessages.unbalanacedCurlyBraces)
931
+ );
932
+ }
933
+ }
934
+
935
+ if (hasErrors) {
936
+ setErrorMessage(newErrors);
937
+ } else {
938
+ // No errors, proceed with submission
939
+ onSuccess();
940
+ }
842
941
  } else {
843
- await onSuccess();
942
+ // No tags available, skip validation and proceed directly
943
+ onSuccess();
844
944
  }
845
945
  };
846
946
 
@@ -945,7 +1045,15 @@ export const InappAdvanced = (props) => {
945
1045
  )}
946
1046
  <CapButton
947
1047
  onClick={async () => {
948
- await liquidMiddleWare();
1048
+ const isLiquidFlow = hasLiquidSupportFeature();
1049
+ const hasTags = tags && tags?.length > 0;
1050
+ if (isLiquidFlow || hasTags) {
1051
+ // Use validation middleware for tag validation
1052
+ await liquidMiddleWare();
1053
+ } else {
1054
+ // No validation needed, proceed directly
1055
+ await onDoneCallback();
1056
+ }
949
1057
  }}
950
1058
  disabled={isDisableDone()}
951
1059
  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 = {
@@ -410,7 +409,6 @@ describe('InappAdvanced Component', () => {
410
409
  actions: mockActions,
411
410
  globalActions: {
412
411
  fetchSchemaForEntity: jest.fn(),
413
- getLiquidTags: jest.fn((content, callback) => callback({ askAiraResponse: { data: [] }, isError: false })),
414
412
  },
415
413
  location: {
416
414
  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,56 +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 or return payload; always return early
101
- if (!this.state.isFormValid) {
102
- if (typeof nextProps.onValidationFail === 'function') {
103
- nextProps.onValidationFail();
104
- }
105
- return;
106
- }
107
- const hasAllValidationCallbacks =
108
- typeof nextProps.getLiquidTags === 'function' &&
109
- typeof nextProps.showLiquidErrorInFooter === 'function' &&
110
- typeof nextProps.onValidationFail === 'function';
111
-
112
- if (hasAllValidationCallbacks) {
113
- const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
114
- validateMobilePushContent(formDataArr, {
115
- currentTab: this.state.currentTab,
116
- getLiquidTags: nextProps.getLiquidTags,
117
- formatMessage: this.props.intl.formatMessage,
118
- messages: formBuilderMessages,
119
- onError: (err) => {
120
- const { standardErrors = [], liquidErrors = [] } = err;
121
- // _validatePlatformSpecificContent passes { standardErrors: { ANDROID, IOS, generic }, liquidErrors: { ... } }; footer expects arrays
122
- const toArray = (v) => (Array.isArray(v) ? v : (v && typeof v === 'object' ? [].concat(...Object.values(v)) : []));
123
- const STANDARD_ERROR_MSG = toArray(standardErrors);
124
- const LIQUID_ERROR_MSG = toArray(liquidErrors);
125
- if (typeof nextProps.showLiquidErrorInFooter === 'function') {
126
- nextProps.showLiquidErrorInFooter(
127
- { STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
128
- this.state.currentTab
129
- );
130
- }
131
- // Only trigger onValidationFail when there are actual errors; skip when helper called onError with empty arrays (reset case)
132
- if ((STANDARD_ERROR_MSG.length > 0 || LIQUID_ERROR_MSG.length > 0) && typeof nextProps.onValidationFail === 'function') {
133
- nextProps.onValidationFail();
134
- }
135
- },
136
- onSuccess: () => {
137
- if (this.state.isFormValid && typeof nextProps.getFormLibraryData === 'function') {
138
- nextProps.getFormLibraryData(this.getFormData());
139
- }
140
- },
141
- });
142
- } else {
143
- // Fail closed: require full validation callback set; treat any missing callback as validation failure
144
- if (typeof nextProps.onValidationFail === 'function') {
145
- nextProps.onValidationFail();
146
- }
147
- }
97
+ if (nextProps.isGetFormData && !this.props.isFullMode) {
98
+ nextProps.getFormLibraryData(this.getFormData());
148
99
  } else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
149
100
  this.startValidation();
150
101
  }
@@ -727,21 +678,31 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
727
678
  }
728
679
  showError = () => {
729
680
  const {intl} = this.props;
730
- const {errorData, schema} = this.state;
681
+ const {errorData} = this.state;
731
682
  const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
732
683
  if (!isEmpty(this.state.formData) && !this.state.isFormValid) {
733
684
  let tab = this.state.currentTab;
734
- const isAndroidInvalid = Object.values(errorData[0] || {}).includes(true);
735
- const isIosInvalid = Object.values(errorData[1] || {}).includes(true);
736
- 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;
737
688
  if (isAndroidInvalid) {
738
689
  tab = 1;
739
690
  errorMessage.description = intl.formatMessage(messages.invalidAndroidMessage);
740
- } 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) {
741
697
  tab = 2;
742
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
+ }
743
704
  }
744
- if (tab !== this.state.currentTab) {
705
+ if (tab !== this.state.currentTab || isTagErrorExist) {
745
706
  CapNotification.error(errorMessage);
746
707
  }
747
708
  }
@@ -2068,7 +2029,6 @@ Create.propTypes = {
2068
2029
  onPreviewContentClicked: PropTypes.func,
2069
2030
  onTestContentClicked: PropTypes.func,
2070
2031
  eventContextTags: PropTypes.array,
2071
- getLiquidTags: PropTypes.func,
2072
2032
  showLiquidErrorInFooter: PropTypes.func,
2073
2033
  showTestAndPreviewSlidebox: PropTypes.bool,
2074
2034
  handleTestAndPreview: PropTypes.func,