@capillarytech/creatives-library 8.0.114 → 8.0.116

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 (42) hide show
  1. package/package.json +1 -1
  2. package/utils/commonUtils.js +354 -4
  3. package/utils/tagValidations.js +22 -5
  4. package/utils/tests/commonUtil.test.js +605 -169
  5. package/utils/tests/tagValidations.test.js +129 -3
  6. package/v2Components/ErrorInfoNote/ErrorTypeRenderer.js +125 -0
  7. package/v2Components/ErrorInfoNote/ErrorTypeRenderer.test.js +147 -0
  8. package/v2Components/ErrorInfoNote/index.js +114 -47
  9. package/v2Components/ErrorInfoNote/messages.js +25 -0
  10. package/v2Components/ErrorInfoNote/style.scss +14 -1
  11. package/v2Components/ErrorInfoNote/utils.js +50 -0
  12. package/v2Components/ErrorInfoNote/utils.test.js +189 -0
  13. package/v2Components/FormBuilder/index.js +203 -127
  14. package/v2Components/FormBuilder/messages.js +1 -1
  15. package/v2Containers/Cap/reducer.js +4 -4
  16. package/v2Containers/Cap/sagas.js +9 -3
  17. package/v2Containers/Cap/tests/saga.test.js +12 -0
  18. package/v2Containers/CreativesContainer/SlideBoxContent.js +26 -3
  19. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
  20. package/v2Containers/CreativesContainer/constants.js +4 -1
  21. package/v2Containers/CreativesContainer/index.js +46 -19
  22. package/v2Containers/CreativesContainer/messages.js +4 -0
  23. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +21 -3
  24. package/v2Containers/CreativesContainer/tests/index.test.js +1 -0
  25. package/v2Containers/Ebill/index.js +3 -3
  26. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +1 -1
  27. package/v2Containers/InApp/index.js +126 -50
  28. package/v2Containers/InApp/tests/index.test.js +1 -1
  29. package/v2Containers/InApp/tests/sagas.test.js +1 -1
  30. package/v2Containers/InApp/tests/utils.test.js +85 -0
  31. package/v2Containers/InApp/utils.js +57 -0
  32. package/v2Containers/InApp/utils.test.js +70 -0
  33. package/v2Containers/MobilePush/Create/index.js +24 -20
  34. package/v2Containers/MobilePush/Edit/index.js +6 -2
  35. package/v2Containers/MobilepushWrapper/index.js +2 -0
  36. package/v2Containers/Sms/Create/index.js +1 -0
  37. package/v2Containers/Sms/Edit/index.js +2 -0
  38. package/v2Containers/SmsTrai/Edit/index.js +49 -10
  39. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +112 -36
  40. package/v2Containers/SmsTrai/Edit/tests/index.test.js +1 -3
  41. package/v2Containers/SmsWrapper/index.js +5 -1
  42. package/v2Containers/Templates/sagas.js +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.114",
4
+ "version": "8.0.116",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -1,9 +1,23 @@
1
1
  import React from 'react';
2
2
  import { FormattedMessage } from 'react-intl';
3
3
  import get from 'lodash/get';
4
- import { EMBEDDED } from '../v2Containers/Whatsapp/constants';
5
- import { EDIT, PREVIEW } from '../v2Containers/App/constants';
6
-
4
+ import { convert } from "html-to-text";
5
+ import { EMBEDDED } from "../v2Containers/Whatsapp/constants";
6
+ import {
7
+ EDIT,
8
+ EMAIL,
9
+ INAPP,
10
+ PREVIEW,
11
+ } from "../v2Containers/App/constants";
12
+ import {
13
+ MOBILE_PUSH,
14
+ SMS,
15
+ ANDROID,
16
+ IOS,
17
+ } from "../v2Containers/CreativesContainer/constants";
18
+ import { GLOBAL_CONVERT_OPTIONS } from "../v2Components/FormBuilder/constants";
19
+ import { checkSupport, extractNames } from "./tagValidations";
20
+ import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
7
21
  export const apiMessageFormatHandler = (id, fallback) => (
8
22
  <FormattedMessage id={id} defaultMessage={fallback} />
9
23
  );
@@ -21,7 +35,7 @@ export const addBaseToTemplate = (template) => {
21
35
  ...template.versions,
22
36
  base: {
23
37
  ...history[0],
24
- ...( !history?.[0]?.subject && { subject: get(template, 'versions.base.subject') }),
38
+ ...( get(template, 'versions.base.subject') ? {subject : get(template, 'versions.base.subject')} :{ subject:history?.[0]?.subject }),
25
39
  },
26
40
  },
27
41
  });
@@ -83,3 +97,339 @@ export const transformCustomFieldsData = (customFields) => {
83
97
  };
84
98
 
85
99
  export const isTagIncluded = (value) => value && value.includes('{{') && value.includes('}}');
100
+
101
+
102
+ /**
103
+ *
104
+ * @param {*} channel
105
+ * @param {*} formData
106
+ * @returns
107
+ */
108
+ export const getChannelData = (channel, formData, baseLanguage) => {
109
+ switch (channel?.toUpperCase()) {
110
+ case EMAIL.toUpperCase():
111
+ return convert(
112
+ formData?.base?.[baseLanguage]?.["template-content"] || "",
113
+ GLOBAL_CONVERT_OPTIONS
114
+ );
115
+
116
+ case SMS:
117
+ return (
118
+ `${formData?.base?.["sms-editor"]} ${formData?.["template-name"]}` || ""
119
+ );
120
+ case SMS_TRAI_VAR?.toUpperCase():
121
+ return formData?.versions?.base?.["updated-sms-editor"].join(" ") || "";
122
+ default:
123
+ return JSON.stringify(formData);
124
+ }
125
+ };
126
+
127
+ /**
128
+ * Validates liquid template content for a given channel and options.
129
+ * @param {string} content - The content to validate.
130
+ * @param {string} channel - Channel type (e.g., EMAIL, SMS, MOBILE_PUSH).
131
+ * @param {object} options - Validation options and callbacks.
132
+ * @returns {Promise} Resolves to the validation result.
133
+ */
134
+ export const validateLiquidTemplateContent = async (
135
+ content,
136
+ options
137
+ ) => {
138
+ const {
139
+ getLiquidTags,
140
+ formatMessage,
141
+ messages,
142
+ onError = () => {},
143
+ onSuccess = () => {},
144
+ tagLookupMap,
145
+ eventContextTags,
146
+ isLiquidFlow,
147
+ forwardedTags = {},
148
+ tabType,
149
+ skipTags = () => false,
150
+ } = options;
151
+ const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
152
+ const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
153
+ // Empty content check
154
+
155
+ if (!content || content.trim() === "") {
156
+ onError({
157
+ standardErrors: [emptyBodyError],
158
+ liquidErrors: [],
159
+ tabType,
160
+ });
161
+ return false;
162
+ }
163
+
164
+ // Await getLiquidTags as a promise if it doesn't already return one
165
+ const getLiquidTagsAsync = (inputContent) => new Promise((resolve) => {
166
+ getLiquidTags(inputContent, (result) => {
167
+ resolve(result);
168
+ });
169
+ });
170
+
171
+ const {askAiraResponse: result, isError} = await getLiquidTagsAsync(content);
172
+ const validString = /\S/.test(content);
173
+
174
+ // Handle API errors or empty content
175
+ if (result?.errors?.length > 0 || !validString || isError) {
176
+ let standardErrors = [];
177
+ if (!validString) {
178
+ standardErrors = [emptyBodyError];
179
+ }
180
+ let liquidErrors;
181
+ if (result && Array.isArray(result?.errors)) {
182
+ liquidErrors = result?.errors?.map((error) => {
183
+ const message = typeof error?.message === "string"
184
+ ? error.message
185
+ : somethingWrongMsg;
186
+ return message;
187
+ });
188
+ } else {
189
+ liquidErrors = [somethingWrongMsg];
190
+ }
191
+ onError({
192
+ standardErrors,
193
+ liquidErrors,
194
+ tabType,
195
+ });
196
+ return false;
197
+ }
198
+ // Extract and validate tags
199
+ const extractedLiquidTags = extractNames(result?.data || []);
200
+ // Get supported tags
201
+ const supportedLiquidTags = checkSupport(
202
+ result,
203
+ tagLookupMap,
204
+ eventContextTags,
205
+ isLiquidFlow,
206
+ forwardedTags
207
+ );
208
+ // Find unsupported tags
209
+ const unsupportedLiquidTags = extractedLiquidTags?.filter(
210
+ (tag) => !supportedLiquidTags?.includes(tag) && !skipTags(tag)
211
+ );
212
+ // Handle unsupported tags
213
+ if (unsupportedLiquidTags?.length > 0) {
214
+ const errorMsg = formatMessage(messages.unsupportedTagsValidationError, {
215
+ unsupportedTags: unsupportedLiquidTags.join(", "),
216
+ });
217
+ onError({
218
+ standardErrors: [],
219
+ liquidErrors: [errorMsg],
220
+ tabType,
221
+ });
222
+ return false;
223
+ }
224
+ // All validations passed
225
+ onSuccess(content, tabType);
226
+ return true;
227
+ };
228
+
229
+ // Internal helper function to validate platform-specific content
230
+ export const _validatePlatformSpecificContent = async (
231
+ androidContent,
232
+ iosContent,
233
+ channel,
234
+ options // Contains parent's onError and other options for validateLiquidTemplateContent
235
+ ) => {
236
+ const {
237
+ singleTab,
238
+ onError: parentOnError, // The onError callback from the calling function (e.g., validateMobilePushContent)
239
+ ...commonVltcOptions // Other options like getLiquidTags, formatMessage, messages, currentTab, etc.
240
+ } = options;
241
+
242
+ let isAndroidValid = false;
243
+ let isIosValid = false;
244
+
245
+ // This aggregator is passed to validateLiquidTemplateContent.
246
+ // It accumulates errors in the new structure and calls the parentOnError
247
+ const internalOnErrorAggregator = ({
248
+ standardErrors = [],
249
+ liquidErrors = [],
250
+ tabType: tabTypeFromVLTC,
251
+ }) => {
252
+
253
+ aggregatedErrors.standardErrors[tabTypeFromVLTC?.toUpperCase() ?? ''].push(
254
+ ...standardErrors
255
+ );
256
+ aggregatedErrors.liquidErrors[tabTypeFromVLTC?.toUpperCase() ?? ''].push(
257
+ ...liquidErrors
258
+ );
259
+ parentOnError(aggregatedErrors);
260
+ };
261
+
262
+ // Base options for calling validateLiquidTemplateContent
263
+ const vltcCallOptions = {
264
+ ...commonVltcOptions,
265
+ onError: internalOnErrorAggregator,
266
+ onSuccess: () => {},
267
+ };
268
+ // Initialize error structure with platform-specific categories
269
+ const aggregatedErrors = {
270
+ standardErrors: {
271
+ [ANDROID]: [],
272
+ [IOS]: [],
273
+ generic: [],
274
+ },
275
+ liquidErrors: {
276
+ [ANDROID]: [],
277
+ [IOS]: [],
278
+ generic: [],
279
+ },
280
+ };
281
+
282
+ if (androidContent && (singleTab === ANDROID || !singleTab)) {
283
+
284
+ isAndroidValid = await validateLiquidTemplateContent(
285
+ androidContent,
286
+ { ...vltcCallOptions, tabType: ANDROID }
287
+ );
288
+ }
289
+
290
+ if (iosContent && (singleTab === IOS || !singleTab)) {
291
+
292
+ isIosValid = await validateLiquidTemplateContent(
293
+ iosContent,
294
+ { ...vltcCallOptions, tabType: IOS }
295
+ );
296
+ }
297
+ //if singleTab is android, then validate only android content and make ios valid by default
298
+ if (singleTab === ANDROID) {
299
+ isIosValid = true;
300
+ }
301
+ //if singleTab is ios, then validate only ios content and make android valid by default
302
+ if (singleTab === IOS) {
303
+ isAndroidValid = true;
304
+ }
305
+
306
+ return isAndroidValid && isIosValid; // Overall success
307
+ };
308
+
309
+ /**
310
+ * Validate Mobile Push content for both Android and iOS tabs
311
+ * @param {object} formData - Form data containing Android and iOS content
312
+ * @param {object} options - Options for validation
313
+ * @returns {Promise} - Promise that resolves when validation completes
314
+ */
315
+ export const validateMobilePushContent = async (formData, options) => {
316
+ const {
317
+ currentTab,
318
+ onError, // This is the onError callback passed by the caller of validateMobilePushContent
319
+ onSuccess, // This is the FINAL success callback for MobilePush
320
+ ...restOptions // Options for validateLiquidTemplateContent (getLiquidTags, formatMessage, etc.)
321
+ } = options;
322
+ // Clear previous errors by calling the passed onError
323
+ onError({ standardErrors: [], liquidErrors: [] });
324
+
325
+ const androidContent = JSON.stringify(formData?.[0]);
326
+ const iosContent = JSON.stringify(formData?.[1]);
327
+
328
+ // Pass the original 'onError' from this function's arguments,
329
+ // 'currentTab', and other relevant options to the helper.
330
+ const overallSuccess = await _validatePlatformSpecificContent(
331
+ androidContent,
332
+ iosContent,
333
+ MOBILE_PUSH,
334
+ { ...restOptions, currentTab, onError }
335
+ );
336
+ const getContentToSubmit = () => {
337
+ if (currentTab === 1 && androidContent) return [androidContent, ANDROID?.toLowerCase()];
338
+ if (currentTab === 2 && iosContent) return [iosContent, IOS?.toLowerCase()];
339
+ if (androidContent) return [androidContent, ANDROID?.toLowerCase()];
340
+ if (iosContent) return [iosContent, IOS?.toLowerCase()];
341
+ return ["", ""];
342
+ };
343
+
344
+ if (overallSuccess) {
345
+ const [contentToSubmit, tabTypeToSubmit] = getContentToSubmit();
346
+ // Call the FINAL onSuccess only ONCE here
347
+ onSuccess(contentToSubmit, tabTypeToSubmit);
348
+ return true;
349
+ }
350
+ return false;
351
+ };
352
+
353
+ // Helper function to extract content for a platform
354
+ export const extractContent = (platformData) => {
355
+ if (!platformData) return '';
356
+ const { title, message, ctas } = platformData;
357
+ return [
358
+ title,
359
+ message,
360
+ ...((ctas?.map((cta) => cta?.text || cta?.actionLink)) || []),
361
+ ].filter(Boolean).join(' ');
362
+ };
363
+
364
+ /**
365
+ * Validate INAPP content for both Android and iOS
366
+ * @param {object} formData - Form data containing Android and iOS content
367
+ * @param {object} options - Options for validation
368
+ * @returns {Promise} - Promise that resolves when validation completes
369
+ */
370
+ export const validateInAppContent = async (formData, options) => {
371
+ const {
372
+ onError, // This is the onError callback passed by the caller of validateInAppContent
373
+ onSuccess, // FINAL success callback
374
+ ...restOptions // Options for validateLiquidTemplateContent
375
+ } = options;
376
+
377
+ // Clear previous errors with new structure
378
+ onError({
379
+ standardErrors: {
380
+ [ANDROID]: [],
381
+ [IOS]: [],
382
+ generic: [],
383
+ },
384
+ liquidErrors: {
385
+ [ANDROID]: [],
386
+ [IOS]: [],
387
+ generic: [],
388
+ },
389
+ });
390
+
391
+ let androidContent = "";
392
+ let iosContent = "";
393
+ let parseError = null;
394
+
395
+ // Extract content
396
+ try {
397
+
398
+ const baseContent = formData?.versions?.base?.content;
399
+ androidContent = extractContent(baseContent?.ANDROID);
400
+ iosContent = extractContent(baseContent?.IOS);
401
+ } catch (error) {
402
+ parseError = error; // Capture parsing error
403
+ }
404
+
405
+ // Check if *any* content exists
406
+ if (parseError) {
407
+ onError((prevErrors) => ({
408
+ ...prevErrors,
409
+ standardErrors: {
410
+ ...prevErrors.standardErrors,
411
+ generic: [
412
+ restOptions.formatMessage(restOptions.messages.somethingWentWrong),
413
+ ],
414
+ },
415
+ }));
416
+ return false;
417
+ }
418
+
419
+ // Pass the original 'onError' from this function's arguments
420
+ // and other relevant options to the helper.
421
+ const overallSuccess = await _validatePlatformSpecificContent(
422
+ androidContent,
423
+ iosContent,
424
+ INAPP,
425
+ { ...restOptions, onError }
426
+ );
427
+
428
+ if (overallSuccess) {
429
+ // Call FINAL onSuccess ONCE
430
+ onSuccess();
431
+ return true;
432
+ }
433
+ // Errors already reported via the 'onError' callback passed to _validatePlatformSpecificContent
434
+ return false;
435
+ };
@@ -19,6 +19,7 @@ const SUBTAGS = 'subtags';
19
19
  * @param {Object} tagObject - The tagLookupMap.
20
20
  */
21
21
  export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [], isLiquidFlow = false, forwardedTags = {}) => {
22
+
22
23
  const supportedList = [];
23
24
  // Verifies the presence of the tag in the 'Add Labels' section.
24
25
  // Incase of journey event context the tags won't be available in the tagObject(tagLookupMap).
@@ -73,6 +74,7 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
73
74
 
74
75
  }
75
76
 
77
+
76
78
  return supportedList;
77
79
  };
78
80
 
@@ -115,6 +117,20 @@ export function extractNames(data) {
115
117
  return names;
116
118
  }
117
119
 
120
+ // Helper to check if a tag is inside a {% ... %} block
121
+ export const isInsideLiquidBlock = (content, tagIndex) => {
122
+ const blockRegex = /{%(.*?)%}/gs;
123
+ let match;
124
+ for (match = blockRegex.exec(content); match !== null; match = blockRegex.exec(content)) {
125
+ const blockStart = match.index;
126
+ const blockEnd = blockStart + match[0].length;
127
+ if (tagIndex >= blockStart && tagIndex < blockEnd) {
128
+ return true;
129
+ }
130
+ }
131
+ return false;
132
+ };
133
+
118
134
  /**
119
135
  * Validates the tags based on the provided parameters.
120
136
  * @param {Object} params - The parameters for tag validation.
@@ -130,7 +146,6 @@ export const validateTags = ({
130
146
  const tags = tagsParam;
131
147
  const injectedTags = transformInjectedTags(injectedTagsParams);
132
148
  let currentModule = location?.query?.module ? location?.query?.module : DEFAULT;
133
-
134
149
  if (tagModule) {
135
150
  currentModule = tagModule;
136
151
  }
@@ -160,6 +175,7 @@ export const validateTags = ({
160
175
  let match = regex.exec(content);
161
176
  while (match !== null) {
162
177
  const tagValue = match[0].substring(indexOfEnd(match[0], '{{'), match[0].indexOf('}}'));
178
+ const tagIndex = match?.index;
163
179
  match = regex.exec(content);
164
180
  let ifSupported = false;
165
181
  lodashForEach(tags, (tag) => {
@@ -170,10 +186,10 @@ export const validateTags = ({
170
186
  const ifSkipped = skipTags(tagValue);
171
187
  if (ifSkipped) {
172
188
  ifSupported = true;
173
- let isUnsubscribeSkipped = tagValue.indexOf("unsubscribe") != -1 ;
189
+ const isUnsubscribeSkipped = tagValue.indexOf("unsubscribe") !== -1;
174
190
  if (isUnsubscribeSkipped) {
175
191
  const missingTagIndex = response.missingTags.indexOf("unsubscribe");
176
- if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
192
+ if (missingTagIndex !== -1) {
177
193
  response.missingTags.splice(missingTagIndex, 1);
178
194
  }
179
195
  }
@@ -185,11 +201,12 @@ export const validateTags = ({
185
201
  }
186
202
  });
187
203
  ifSupported = ifSupported || checkIfSupportedTag(tagValue, injectedTags);
188
- if (!ifSupported) {
204
+ // Only add to unsupportedTags if not inside a {% ... %} block and does not contain a dot
205
+ if (!ifSupported && !isInsideLiquidBlock(content, tagIndex) && tagValue?.indexOf('.') === -1) {
189
206
  response.unsupportedTags.push(tagValue);
190
207
  response.valid = false;
191
208
  }
192
- if (response.unsupportedTags.length == 0 && response.missingTags.length == 0 ) {
209
+ if (response.unsupportedTags.length === 0 && response.missingTags.length === 0) {
193
210
  response.valid = true;
194
211
  }
195
212
  }