@capillarytech/creatives-library 8.0.308 → 8.0.309

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 +5 -1
  2. package/initialState.js +0 -2
  3. package/package.json +1 -1
  4. package/utils/common.js +5 -8
  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/ErrorInfoNote/index.js +2 -5
  10. package/v2Components/FormBuilder/index.js +137 -203
  11. package/v2Components/FormBuilder/messages.js +0 -8
  12. package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
  15. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
  16. package/v2Containers/Cap/mockData.js +0 -14
  17. package/v2Containers/Cap/reducer.js +3 -55
  18. package/v2Containers/Cap/tests/reducer.test.js +0 -102
  19. package/v2Containers/CreativesContainer/SlideBoxContent.js +5 -1
  20. package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -5
  21. package/v2Containers/CreativesContainer/constants.js +6 -0
  22. package/v2Containers/CreativesContainer/index.js +47 -7
  23. package/v2Containers/Email/index.js +1 -5
  24. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +23 -70
  25. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -120
  26. package/v2Containers/FTP/index.js +2 -51
  27. package/v2Containers/FTP/messages.js +0 -4
  28. package/v2Containers/InApp/index.js +35 -107
  29. package/v2Containers/InApp/tests/index.test.js +17 -6
  30. package/v2Containers/InappAdvance/index.js +4 -112
  31. package/v2Containers/InappAdvance/tests/index.test.js +2 -0
  32. package/v2Containers/Line/Container/Text/index.js +0 -1
  33. package/v2Containers/MobilePush/Create/index.js +59 -19
  34. package/v2Containers/MobilePush/Edit/index.js +48 -20
  35. package/v2Containers/MobilePushNew/index.js +12 -32
  36. package/v2Containers/MobilepushWrapper/index.js +3 -1
  37. package/v2Containers/Rcs/index.js +12 -37
  38. package/v2Containers/Sms/Create/index.js +39 -3
  39. package/v2Containers/Sms/Create/messages.js +4 -0
  40. package/v2Containers/Sms/Edit/index.js +35 -3
  41. package/v2Containers/Sms/commonMethods.js +3 -6
  42. package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
  43. package/v2Containers/SmsTrai/Edit/index.js +11 -47
  44. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  45. package/v2Containers/SmsWrapper/index.js +2 -0
  46. package/v2Containers/TemplatesV2/index.js +28 -13
  47. package/v2Containers/Viber/index.js +0 -1
  48. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
  49. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  50. package/v2Containers/WebPush/Create/index.js +2 -2
  51. package/v2Containers/WebPush/Create/utils/validation.js +17 -8
  52. package/v2Containers/WebPush/Create/utils/validation.test.js +44 -24
  53. package/v2Containers/Whatsapp/index.js +9 -17
  54. package/v2Containers/Zalo/index.js +3 -11
@@ -33,7 +33,6 @@ 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";
37
36
  import injectReducer from '../../utils/injectReducer';
38
37
  import v2InAppReducer from './reducer';
39
38
  import { v2InAppSagas } from './sagas';
@@ -53,7 +52,7 @@ import {
53
52
  LAYOUT_RADIO_OPTIONS,
54
53
  IOS_CAPITAL,
55
54
  } from "./constants";
56
- import { INAPP, SMS } from "../CreativesContainer/constants";
55
+ import { GENERIC, INAPP, SMS } from "../CreativesContainer/constants";
57
56
  import { AI_CONTENT_BOT_DISABLED } from "../../constants/unified";
58
57
  import {
59
58
  ALL, TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
@@ -61,7 +60,8 @@ import {
61
60
  import { getCdnUrl } from "../../utils/cdnTransformation";
62
61
  import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
63
62
  import { validateInAppContent } from "../../utils/commonUtils";
64
- import { hasLiquidSupportFeature, hasNewEditorFlowInAppEnabled } from "../../utils/common";
63
+ import { validateTags } from "../../utils/tagValidations";
64
+ import { 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,39 +1084,55 @@ 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
1087
1088
  const validationMiddleWare = async () => {
1088
- // Set up callbacks for validation results
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
+ };
1089
1115
  const onError = ({ standardErrors, liquidErrors }) => {
1090
1116
  setErrorMessage((prev) => ({
1091
- STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
1092
- LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
1117
+ STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...normalizeErrorBuckets(standardErrors) },
1118
+ LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...normalizeErrorBuckets(liquidErrors) },
1093
1119
  }));
1094
1120
  };
1095
1121
  const onSuccess = () => {
1096
- // Proceed with submission when validation is successful
1097
1122
  onDoneCallback();
1098
1123
  };
1099
1124
 
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
1125
+ // Library mode: run extractTags validation (always, so we catch liquid errors even when tags not loaded)
1126
+ if (!isFullMode) {
1106
1127
  const payload = createPayload();
1107
1128
  validateInAppContent(payload, {
1108
- currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
1129
+ currentTab: panes === ANDROID ? 1 : 2,
1109
1130
  onError,
1110
1131
  onSuccess,
1111
1132
  getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
1112
1133
  formatMessage,
1113
1134
  messages: formBuilderMessages,
1114
- tagLookupMap: metaEntities?.tagLookupMap || {},
1115
- eventContextTags: metaEntities?.eventContextTags || [],
1116
- isLiquidFlow,
1117
- forwardedTags: {},
1118
1135
  skipTags: (tag) => {
1119
- // Skip certain tags if needed
1120
1136
  const skipRegexes = [
1121
1137
  /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
1122
1138
  /unsubscribe\(#[a-zA-Z\d]{6}\)/,
@@ -1124,103 +1140,15 @@ export const InApp = (props) => {
1124
1140
  /SURVEY.*\.TOKEN/,
1125
1141
  /^[A-Za-z].*\([a-zA-Z\d]*\)/,
1126
1142
  ];
1127
-
1128
1143
  return skipRegexes.some((regex) => regex.test(tag));
1129
1144
  },
1130
1145
  singleTab: getSingleTab(accountData),
1131
1146
  });
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
- }
1216
1147
  } else {
1217
- // No tags available, skip validation and proceed directly
1218
1148
  onSuccess();
1219
1149
  }
1220
1150
  };
1221
1151
 
1222
- const isLiquidFlow = hasLiquidSupportFeature();
1223
-
1224
1152
  // Check template data to determine editor type (for render decision)
1225
1153
  const templateDetails = isFullMode ? editData?.templateDetails : templateData;
1226
1154
  const versions = templateDetails?.versions || {};
@@ -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,
@@ -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";
@@ -818,30 +816,18 @@ export const InappAdvanced = (props) => {
818
816
  const payload = createPayload(latestHtmlValues);
819
817
 
820
818
  // Validate the INAPP content
821
- const isLiquidFlow = hasLiquidSupportFeature();
822
819
  // Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
823
820
  const hasTags = tags && tags.length > 0;
824
821
 
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, {
822
+ if (!isFullMode || hasTags) {
823
+ await validateInAppContent(payload, {
833
824
  currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
834
825
  onError,
835
826
  onSuccess,
836
827
  getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
837
828
  formatMessage,
838
829
  messages: formBuilderMessages,
839
- tagLookupMap: metaEntities?.tagLookupMap || {},
840
- eventContextTags: metaEntities?.eventContextTags || [],
841
- isLiquidFlow,
842
- forwardedTags: {},
843
830
  skipTags: (tag) => {
844
- // Skip certain tags if needed
845
831
  const skipRegexes = [
846
832
  /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
847
833
  /unsubscribe\(#[a-zA-Z\d]{6}\)/,
@@ -849,98 +835,12 @@ export const InappAdvanced = (props) => {
849
835
  /SURVEY.*\.TOKEN/,
850
836
  /^[A-Za-z].*\([a-zA-Z\d]*\)/,
851
837
  ];
852
-
853
838
  return skipRegexes.some((regex) => regex.test(tag));
854
839
  },
855
840
  singleTab: getSingleTab(accountData),
856
841
  });
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
- }
941
842
  } else {
942
- // No tags available, skip validation and proceed directly
943
- onSuccess();
843
+ await onSuccess();
944
844
  }
945
845
  };
946
846
 
@@ -1045,15 +945,7 @@ export const InappAdvanced = (props) => {
1045
945
  )}
1046
946
  <CapButton
1047
947
  onClick={async () => {
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
- }
948
+ await liquidMiddleWare();
1057
949
  }}
1058
950
  disabled={isDisableDone()}
1059
951
  className="inapp-create-btn"
@@ -61,6 +61,7 @@ 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 })),
64
65
  };
65
66
 
66
67
  defaultProps = {
@@ -409,6 +410,7 @@ describe('InappAdvanced Component', () => {
409
410
  actions: mockActions,
410
411
  globalActions: {
411
412
  fetchSchemaForEntity: jest.fn(),
413
+ getLiquidTags: jest.fn((content, callback) => callback({ askAiraResponse: { data: [] }, isError: false })),
412
414
  },
413
415
  location: {
414
416
  pathname: '/inapp/create',
@@ -137,7 +137,6 @@ export const LineText = ({
137
137
  const { valid, isBraceError } = validateTags({
138
138
  content: value,
139
139
  tagsParam: tags,
140
- injectedTagsParams: injectedTags,
141
140
  location,
142
141
  tagModule: 'outbound',
143
142
  isFullMode,
@@ -38,7 +38,8 @@ 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 } from '../../../utils/commonUtils';
41
+ import { checkForPersonalizationTokens, validateMobilePushContent } from '../../../utils/commonUtils';
42
+ import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
42
43
 
43
44
  const PrefixWrapper = styled.div`
44
45
  margin-right: 16px;
@@ -94,8 +95,56 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
94
95
  this.props.globalActions.fetchSchemaForEntity(query);
95
96
  };
96
97
  componentWillReceiveProps = (nextProps) => {
97
- if (nextProps.isGetFormData && !this.props.isFullMode) {
98
- nextProps.getFormLibraryData(this.getFormData());
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
+ }
99
148
  } else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
100
149
  this.startValidation();
101
150
  }
@@ -678,31 +727,21 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
678
727
  }
679
728
  showError = () => {
680
729
  const {intl} = this.props;
681
- const {errorData} = this.state;
730
+ const {errorData, schema} = this.state;
682
731
  const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
683
732
  if (!isEmpty(this.state.formData) && !this.state.isFormValid) {
684
733
  let tab = this.state.currentTab;
685
- const isAndroidInvalid = Object.values(errorData[0]).includes(true);
686
- const isIosInvalid = Object.values(errorData[1]).includes(true);
687
- let isTagErrorExist = false;
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;
688
737
  if (isAndroidInvalid) {
689
738
  tab = 1;
690
739
  errorMessage.description = intl.formatMessage(messages.invalidAndroidMessage);
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) {
740
+ } else if (isIosInvalid && isIosTabVisible) {
697
741
  tab = 2;
698
742
  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
- }
704
743
  }
705
- if (tab !== this.state.currentTab || isTagErrorExist) {
744
+ if (tab !== this.state.currentTab) {
706
745
  CapNotification.error(errorMessage);
707
746
  }
708
747
  }
@@ -2029,6 +2068,7 @@ Create.propTypes = {
2029
2068
  onPreviewContentClicked: PropTypes.func,
2030
2069
  onTestContentClicked: PropTypes.func,
2031
2070
  eventContextTags: PropTypes.array,
2071
+ getLiquidTags: PropTypes.func,
2032
2072
  showLiquidErrorInFooter: PropTypes.func,
2033
2073
  showTestAndPreviewSlidebox: PropTypes.bool,
2034
2074
  handleTestAndPreview: PropTypes.func,
@@ -39,7 +39,8 @@ import { v2MobilePushEditSagas } from './sagas';
39
39
  import v2MobilePushEditReducer from './reducer';
40
40
  import * as globalActions from '../../Cap/actions';
41
41
  import { MAPP_SDK } from './constants';
42
- import { checkForPersonalizationTokens, isEmbeddedEditOrPreview } from '../../../utils/commonUtils';
42
+ import { checkForPersonalizationTokens, isEmbeddedEditOrPreview, validateMobilePushContent } from '../../../utils/commonUtils';
43
+ import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
43
44
  import { EMBEDDED } from '../../Whatsapp/constants';
44
45
  import { OUTBOUND } from '../../../v2Components/FormBuilder/constants';
45
46
  import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
@@ -68,6 +69,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
68
69
  schema: {},
69
70
  currentTab: 1,
70
71
  editData: {},
72
+ errorData: [],
71
73
  loading: false,
72
74
  isFormValid: true,
73
75
  injectedTags: {},
@@ -129,8 +131,43 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
129
131
  this.hasFetchedInitialTagsRef = false;
130
132
  this.lastFetchedTagContextRef = null;
131
133
  }
132
- if (nextProps.isGetFormData && !this.props.isFullMode) {
133
- nextProps.getFormLibraryData(this.getFormData());
134
+ // Library mode: on Done click (transition to isGetFormData), run extractTags only when no existing validation errors (braces, personalization, etc.)
135
+ if (nextProps.isGetFormData && !this.props.isGetFormData && !nextProps.isFullMode) {
136
+ // If form already has validation errors (braces, personalization tags, etc.), do not call extractTags; just fail
137
+ if (!this.state.isFormValid) {
138
+ nextProps.onValidationFail?.();
139
+ return;
140
+ }
141
+ if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter) {
142
+ const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
143
+ validateMobilePushContent(formDataArr, {
144
+ currentTab: this.state.currentTab,
145
+ getLiquidTags: nextProps.getLiquidTags,
146
+ formatMessage: this.props.intl.formatMessage,
147
+ messages: formBuilderMessages,
148
+ onError: (err) => {
149
+ const { standardErrors = [], liquidErrors = [] } = err;
150
+ // _validatePlatformSpecificContent passes { standardErrors: { ANDROID, IOS, generic }, liquidErrors: { ... } }; footer expects arrays
151
+ const toArray = (v) => (Array.isArray(v) ? v : (v && typeof v === 'object' ? [].concat(...Object.values(v)) : []));
152
+ const STANDARD_ERROR_MSG = toArray(standardErrors);
153
+ const LIQUID_ERROR_MSG = toArray(liquidErrors);
154
+ nextProps.showLiquidErrorInFooter(
155
+ { STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
156
+ this.state.currentTab
157
+ );
158
+ // Only treat as validation failure when there are actual errors; skip when helper called onError with empty reset payload
159
+ const hasErrors = STANDARD_ERROR_MSG.length > 0 || LIQUID_ERROR_MSG.length > 0;
160
+ if (hasErrors) {
161
+ nextProps.onValidationFail?.();
162
+ }
163
+ },
164
+ onSuccess: () => {
165
+ nextProps.getFormLibraryData(this.getFormData());
166
+ },
167
+ });
168
+ } else {
169
+ nextProps.getFormLibraryData(this.getFormData());
170
+ }
134
171
  } else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
135
172
  this.startValidation();
136
173
  }
@@ -705,32 +742,22 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
705
742
  // eslint-disable-next-line react/sort-comp
706
743
  showError = () => {
707
744
  const {intl} = this.props;
708
- const {errorData} = this.state;
745
+ const {errorData, schema} = this.state;
709
746
  const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
710
- let isTagErrorExist = false;
711
747
  if (!_.isEmpty(this.state.formData) && !this.state.isFormValid) {
712
748
  let tab = this.state.currentTab;
713
- const isAndroidInvalid = Object.values(errorData[0]).includes(true);
714
- const isIosInvalid = Object.values(errorData[1]).includes(true);
749
+ const isAndroidInvalid = Object.values(errorData?.[0] || {}).includes(true);
750
+ const isIosInvalid = Object.values(errorData?.[1] || {}).includes(true);
751
+ const isIosTabVisible = get(schema, 'containers[0].panes[1].isSupported', true) !== false;
715
752
  if (isAndroidInvalid) {
716
753
  tab = 1;
717
754
  errorMessage.description = intl.formatMessage(messages.invalidAndroidMessage);
718
- const invalidTags = errorData[0]['invalid-tags'];
719
- if (!_.isEmpty(invalidTags)) {
720
- isTagErrorExist = true;
721
- errorMessage.description = `${intl.formatMessage(messages.invalidAndroidMessage)} ${intl.formatMessage(messages.invalidTags)}: ${invalidTags.join(',')} `;
722
- }
723
- } else if (isIosInvalid) {
755
+ } else if (isIosInvalid && isIosTabVisible) {
724
756
  tab = 2;
725
757
  errorMessage.description = intl.formatMessage(messages.invalidIosMessage);
726
- const invalidTags = errorData[1]['invalid-tags'];
727
- if (!_.isEmpty(invalidTags)) {
728
- isTagErrorExist = true;
729
- errorMessage.description = `${intl.formatMessage(messages.invalidIosMessage)} ${intl.formatMessage(messages.invalidTags)}: ${invalidTags.join(',')} `;
730
- }
731
758
  }
732
759
 
733
- if (tab !== this.state.currentTab || isTagErrorExist) {
760
+ if (tab !== this.state.currentTab) {
734
761
  CapNotification.error(errorMessage);
735
762
  }
736
763
  }
@@ -2307,11 +2334,12 @@ Edit.propTypes = {
2307
2334
  getFormLibraryData: PropTypes.func,
2308
2335
  isGetFormData: PropTypes.bool,
2309
2336
  Create: PropTypes.object,
2310
- onValidationFail: PropTypes.bool,
2337
+ onValidationFail: PropTypes.func,
2311
2338
  onPreviewContentClicked: PropTypes.func,
2312
2339
  onTestContentClicked: PropTypes.func,
2313
2340
  creativesMode: PropTypes.string,
2314
2341
  eventContextTags: PropTypes.array,
2342
+ getLiquidTags: PropTypes.func,
2315
2343
  showLiquidErrorInFooter: PropTypes.func,
2316
2344
  showTestAndPreviewSlidebox: PropTypes.bool,
2317
2345
  handleTestAndPreview: PropTypes.func,