@capillarytech/creatives-library 8.0.291 → 8.0.292-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 (52) hide show
  1. package/constants/unified.js +3 -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 +4 -85
  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/index.js +30 -7
  22. package/v2Containers/Email/index.js +1 -5
  23. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +23 -70
  24. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +29 -137
  25. package/v2Containers/FTP/index.js +2 -51
  26. package/v2Containers/FTP/messages.js +0 -4
  27. package/v2Containers/InApp/index.js +4 -104
  28. package/v2Containers/InApp/tests/index.test.js +17 -6
  29. package/v2Containers/InappAdvance/index.js +4 -108
  30. package/v2Containers/InappAdvance/tests/index.test.js +2 -0
  31. package/v2Containers/Line/Container/Text/index.js +0 -1
  32. package/v2Containers/MobilePush/Create/index.js +42 -19
  33. package/v2Containers/MobilePush/Edit/index.js +42 -19
  34. package/v2Containers/MobilePushNew/index.js +12 -32
  35. package/v2Containers/MobilepushWrapper/index.js +3 -1
  36. package/v2Containers/Rcs/index.js +12 -37
  37. package/v2Containers/Sms/Create/index.js +39 -3
  38. package/v2Containers/Sms/Create/messages.js +4 -0
  39. package/v2Containers/Sms/Edit/index.js +35 -3
  40. package/v2Containers/Sms/commonMethods.js +3 -6
  41. package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
  42. package/v2Containers/SmsTrai/Edit/index.js +11 -47
  43. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  44. package/v2Containers/SmsWrapper/index.js +2 -0
  45. package/v2Containers/Viber/index.js +0 -1
  46. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
  47. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  48. package/v2Containers/WebPush/Create/index.js +2 -2
  49. package/v2Containers/WebPush/Create/utils/validation.js +17 -2
  50. package/v2Containers/WebPush/Create/utils/validation.test.js +59 -24
  51. package/v2Containers/Whatsapp/index.js +9 -17
  52. package/v2Containers/Zalo/index.js +3 -11
@@ -21,10 +21,6 @@ export default defineMessages({
21
21
  id: 'creatives.containersV2.FTP.addColumn',
22
22
  defaultMessage: 'Add column',
23
23
  },
24
- unsupportedTagsValidationError: {
25
- id: 'creatives.containersV2.FTP.unsupportedTagsValidationError',
26
- defaultMessage: 'Unsupported tags: {unsupportedTags}. Please remove them from this message.',
27
- },
28
24
  selectTag: {
29
25
  id: 'creatives.containersV2.FTP.selectTag',
30
26
  defaultMessage: 'Select tag',
@@ -31,7 +31,6 @@ import creativesMessages from '../CreativesContainer/messages';
31
31
  import withCreatives from "../../hoc/withCreatives";
32
32
  import UnifiedPreview from "../../v2Components/CommonTestAndPreview/UnifiedPreview";
33
33
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
34
- import { validateTags } from "../../utils/tagValidations";
35
34
  import injectReducer from '../../utils/injectReducer';
36
35
  import v2InAppReducer from './reducer';
37
36
  import { v2InAppSagas } from './sagas';
@@ -58,7 +57,6 @@ import {
58
57
  import { getCdnUrl } from "../../utils/cdnTransformation";
59
58
  import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
60
59
  import { validateInAppContent } from "../../utils/commonUtils";
61
- import { hasLiquidSupportFeature } from "../../utils/common";
62
60
  import formBuilderMessages from "../../v2Components/FormBuilder/messages";
63
61
  import HTMLEditor from "../../v2Components/HtmlEditor";
64
62
  import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
@@ -1079,8 +1077,8 @@ export const InApp = (props) => {
1079
1077
  };
1080
1078
 
1081
1079
  // Validation middleware for tag validation (both liquid and non-liquid flow)
1080
+ // Liquid validation (extractTags) runs only in library mode
1082
1081
  const validationMiddleWare = async () => {
1083
- // Set up callbacks for validation results
1084
1082
  const onError = ({ standardErrors, liquidErrors }) => {
1085
1083
  setErrorMessage((prev) => ({
1086
1084
  STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
@@ -1088,30 +1086,20 @@ export const InApp = (props) => {
1088
1086
  }));
1089
1087
  };
1090
1088
  const onSuccess = () => {
1091
- // Proceed with submission when validation is successful
1092
1089
  onDoneCallback();
1093
1090
  };
1094
1091
 
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
1092
+ // Library mode: run extractTags validation (always, so we catch liquid errors even when tags not loaded)
1093
+ if (!isFullMode) {
1101
1094
  const payload = createPayload();
1102
1095
  validateInAppContent(payload, {
1103
- currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
1096
+ currentTab: panes === ANDROID ? 1 : 2,
1104
1097
  onError,
1105
1098
  onSuccess,
1106
1099
  getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
1107
1100
  formatMessage,
1108
1101
  messages: formBuilderMessages,
1109
- tagLookupMap: metaEntities?.tagLookupMap || {},
1110
- eventContextTags: metaEntities?.eventContextTags || [],
1111
- isLiquidFlow,
1112
- forwardedTags: {},
1113
1102
  skipTags: (tag) => {
1114
- // Skip certain tags if needed
1115
1103
  const skipRegexes = [
1116
1104
  /dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
1117
1105
  /unsubscribe\(#[a-zA-Z\d]{6}\)/,
@@ -1119,103 +1107,15 @@ export const InApp = (props) => {
1119
1107
  /SURVEY.*\.TOKEN/,
1120
1108
  /^[A-Za-z].*\([a-zA-Z\d]*\)/,
1121
1109
  ];
1122
-
1123
1110
  return skipRegexes.some((regex) => regex.test(tag));
1124
1111
  },
1125
1112
  singleTab: getSingleTab(accountData),
1126
1113
  });
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
1114
  } else {
1212
- // No tags available, skip validation and proceed directly
1213
1115
  onSuccess();
1214
1116
  }
1215
1117
  };
1216
1118
 
1217
- const isLiquidFlow = hasLiquidSupportFeature();
1218
-
1219
1119
  // Check template data to determine editor type (for render decision)
1220
1120
  const templateDetails = isFullMode ? editData?.templateDetails : templateData;
1221
1121
  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";
@@ -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) {
818
+ // Liquid validation (extractTags) only in library mode
819
+ if (!isFullMode) {
825
820
  validateInAppContent(payload, {
826
- currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
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,97 +832,11 @@ 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
840
  onSuccess();
937
841
  }
938
842
  };
@@ -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"
@@ -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,39 @@ 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; just fail
101
+ if (!this.state.isFormValid && nextProps.onValidationFail) {
102
+ nextProps.onValidationFail();
103
+ return;
104
+ }
105
+ if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter && nextProps.onValidationFail) {
106
+ const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
107
+ validateMobilePushContent(formDataArr, {
108
+ currentTab: this.state.currentTab,
109
+ getLiquidTags: nextProps.getLiquidTags,
110
+ formatMessage: this.props.intl.formatMessage,
111
+ messages: formBuilderMessages,
112
+ onError: (err) => {
113
+ const { standardErrors = [], liquidErrors = [] } = err;
114
+ // _validatePlatformSpecificContent passes { standardErrors: { ANDROID, IOS, generic }, liquidErrors: { ... } }; footer expects arrays
115
+ const toArray = (v) => (Array.isArray(v) ? v : (v && typeof v === 'object' ? [].concat(...Object.values(v)) : []));
116
+ const STANDARD_ERROR_MSG = toArray(standardErrors);
117
+ const LIQUID_ERROR_MSG = toArray(liquidErrors);
118
+ nextProps.showLiquidErrorInFooter(
119
+ { STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
120
+ this.state.currentTab
121
+ );
122
+ nextProps.onValidationFail();
123
+ },
124
+ onSuccess: () => {
125
+ nextProps.getFormLibraryData(this.getFormData());
126
+ },
127
+ });
128
+ } else {
129
+ nextProps.getFormLibraryData(this.getFormData());
130
+ }
99
131
  } else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
100
132
  this.startValidation();
101
133
  }
@@ -678,31 +710,21 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
678
710
  }
679
711
  showError = () => {
680
712
  const {intl} = this.props;
681
- const {errorData} = this.state;
713
+ const {errorData, schema} = this.state;
682
714
  const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
683
715
  if (!isEmpty(this.state.formData) && !this.state.isFormValid) {
684
716
  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;
717
+ const isAndroidInvalid = Object.values(errorData[0] || {}).includes(true);
718
+ const isIosInvalid = Object.values(errorData[1] || {}).includes(true);
719
+ const isIosTabVisible = get(schema, 'containers[0].panes[1].isSupported', true) !== false;
688
720
  if (isAndroidInvalid) {
689
721
  tab = 1;
690
722
  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) {
723
+ } else if (isIosInvalid && isIosTabVisible) {
697
724
  tab = 2;
698
725
  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
726
  }
705
- if (tab !== this.state.currentTab || isTagErrorExist) {
727
+ if (tab !== this.state.currentTab) {
706
728
  CapNotification.error(errorMessage);
707
729
  }
708
730
  }
@@ -2029,6 +2051,7 @@ Create.propTypes = {
2029
2051
  onPreviewContentClicked: PropTypes.func,
2030
2052
  onTestContentClicked: PropTypes.func,
2031
2053
  eventContextTags: PropTypes.array,
2054
+ getLiquidTags: PropTypes.func,
2032
2055
  showLiquidErrorInFooter: PropTypes.func,
2033
2056
  showTestAndPreviewSlidebox: PropTypes.bool,
2034
2057
  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';
@@ -129,8 +130,39 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
129
130
  this.hasFetchedInitialTagsRef = false;
130
131
  this.lastFetchedTagContextRef = null;
131
132
  }
132
- if (nextProps.isGetFormData && !this.props.isFullMode) {
133
- nextProps.getFormLibraryData(this.getFormData());
133
+ // Library mode: on Done click (transition to isGetFormData), run extractTags only when no existing validation errors (braces, personalization, etc.)
134
+ if (nextProps.isGetFormData && !this.props.isGetFormData && !nextProps.isFullMode) {
135
+ // If form already has validation errors (braces, personalization tags, etc.), do not call extractTags; just fail
136
+ if (!this.state.isFormValid && nextProps.onValidationFail) {
137
+ nextProps.onValidationFail();
138
+ return;
139
+ }
140
+ if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter && nextProps.onValidationFail) {
141
+ const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
142
+ validateMobilePushContent(formDataArr, {
143
+ currentTab: this.state.currentTab,
144
+ getLiquidTags: nextProps.getLiquidTags,
145
+ formatMessage: this.props.intl.formatMessage,
146
+ messages: formBuilderMessages,
147
+ onError: (err) => {
148
+ const { standardErrors = [], liquidErrors = [] } = err;
149
+ // _validatePlatformSpecificContent passes { standardErrors: { ANDROID, IOS, generic }, liquidErrors: { ... } }; footer expects arrays
150
+ const toArray = (v) => (Array.isArray(v) ? v : (v && typeof v === 'object' ? [].concat(...Object.values(v)) : []));
151
+ const STANDARD_ERROR_MSG = toArray(standardErrors);
152
+ const LIQUID_ERROR_MSG = toArray(liquidErrors);
153
+ nextProps.showLiquidErrorInFooter(
154
+ { STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
155
+ this.state.currentTab
156
+ );
157
+ nextProps.onValidationFail();
158
+ },
159
+ onSuccess: () => {
160
+ nextProps.getFormLibraryData(this.getFormData());
161
+ },
162
+ });
163
+ } else {
164
+ nextProps.getFormLibraryData(this.getFormData());
165
+ }
134
166
  } else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
135
167
  this.startValidation();
136
168
  }
@@ -705,32 +737,22 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
705
737
  // eslint-disable-next-line react/sort-comp
706
738
  showError = () => {
707
739
  const {intl} = this.props;
708
- const {errorData} = this.state;
740
+ const {errorData, schema} = this.state;
709
741
  const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
710
- let isTagErrorExist = false;
711
742
  if (!_.isEmpty(this.state.formData) && !this.state.isFormValid) {
712
743
  let tab = this.state.currentTab;
713
- const isAndroidInvalid = Object.values(errorData[0]).includes(true);
714
- const isIosInvalid = Object.values(errorData[1]).includes(true);
744
+ const isAndroidInvalid = Object.values(errorData[0] || {}).includes(true);
745
+ const isIosInvalid = Object.values(errorData[1] || {}).includes(true);
746
+ const isIosTabVisible = get(schema, 'containers[0].panes[1].isSupported', true) !== false;
715
747
  if (isAndroidInvalid) {
716
748
  tab = 1;
717
749
  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) {
750
+ } else if (isIosInvalid && isIosTabVisible) {
724
751
  tab = 2;
725
752
  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
753
  }
732
754
 
733
- if (tab !== this.state.currentTab || isTagErrorExist) {
755
+ if (tab !== this.state.currentTab) {
734
756
  CapNotification.error(errorMessage);
735
757
  }
736
758
  }
@@ -2312,6 +2334,7 @@ Edit.propTypes = {
2312
2334
  onTestContentClicked: PropTypes.func,
2313
2335
  creativesMode: PropTypes.string,
2314
2336
  eventContextTags: PropTypes.array,
2337
+ getLiquidTags: PropTypes.func,
2315
2338
  showLiquidErrorInFooter: PropTypes.func,
2316
2339
  showTestAndPreviewSlidebox: PropTypes.bool,
2317
2340
  handleTestAndPreview: PropTypes.func,