@capillarytech/creatives-library 8.0.225 → 8.0.227

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.
@@ -149,6 +149,7 @@ export const BADGES_ISSUE = 'BADGES_ISSUE';
149
149
  export const CUSTOMER_BARCODE_TAG = 'customer_barcode';
150
150
  export const COPY_OF = 'Copy of';
151
151
  export const ENTRY_TRIGGER_TAG_REGEX = /\bentryTrigger\.\w+(?:\.\w+)?(?:\(\w+\))?/g;
152
+ export const SKIP_TAGS_REGEX_GROUPS = ["dynamic_expiry_date_after_\\d+_days.FORMAT_\\d", "unsubscribe\\(#[a-zA-Z\\d]{6}\\)", "Link_to_[a-zA-Z]", "SURVEY.*.TOKEN", "^[A-Za-z].*\\([a-zA-Z\\d]*\\)", "referral_unique_(code|url).*userid"];
152
153
 
153
154
  export const GET_TRANSLATION_MAPPED = {
154
155
  'en': 'en-US',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.225",
4
+ "version": "8.0.227",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -16,7 +16,7 @@ import {
16
16
  IOS,
17
17
  } from "../v2Containers/CreativesContainer/constants";
18
18
  import { GLOBAL_CONVERT_OPTIONS } from "../v2Components/FormBuilder/constants";
19
- import { checkSupport, extractNames } from "./tagValidations";
19
+ import { checkSupport, extractNames, skipTags as defaultSkipTags } from "./tagValidations";
20
20
  import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
21
21
  export const apiMessageFormatHandler = (id, fallback) => (
22
22
  <FormattedMessage id={id} defaultMessage={fallback} />
@@ -146,7 +146,7 @@ export const validateLiquidTemplateContent = async (
146
146
  isLiquidFlow,
147
147
  forwardedTags = {},
148
148
  tabType,
149
- skipTags = () => false,
149
+ skipTags = defaultSkipTags,
150
150
  } = options;
151
151
  const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
152
152
  const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
@@ -8,7 +8,7 @@
8
8
 
9
9
  import lodashForEach from 'lodash/forEach';
10
10
  import lodashCloneDeep from 'lodash/cloneDeep';
11
- import { ENTRY_TRIGGER_TAG_REGEX } from '../constants/unified';
11
+ import { ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS } from '../constants/unified';
12
12
 
13
13
  const DEFAULT = 'default';
14
14
  const SUBTAGS = 'subtags';
@@ -19,7 +19,6 @@ const SUBTAGS = 'subtags';
19
19
  * @param {Object} tagObject - The tagLookupMap.
20
20
  */
21
21
  export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [], isLiquidFlow = false, forwardedTags = {}) => {
22
-
23
22
  const supportedList = [];
24
23
  // Verifies the presence of the tag in the 'Add Labels' section.
25
24
  // Incase of journey event context the tags won't be available in the tagObject(tagLookupMap).
@@ -40,7 +39,7 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
40
39
  let updatedChildName = childName;
41
40
  let updatedWithoutDotChildName = childName;
42
41
  if (childName?.includes(".")) {
43
- updatedChildName = "." + childName?.split(".")?.[1];
42
+ updatedChildName = `.${childName?.split(".")?.[1]}`;
44
43
  updatedWithoutDotChildName = childName?.split(".")?.[1];
45
44
  }
46
45
  if (tagObject?.[parentTag]) {
@@ -71,7 +70,6 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
71
70
  if (item?.children?.length) {
72
71
  processChildren(item?.name, item?.children);
73
72
  }
74
-
75
73
  }
76
74
 
77
75
 
@@ -80,19 +78,19 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
80
78
 
81
79
  const handleForwardedTags = (forwardedTags) => {
82
80
  const result = [];
83
- Object.keys(forwardedTags).forEach(key => {
81
+ Object.keys(forwardedTags).forEach((key) => {
84
82
  result.push(key); // Add the main key to the result array
85
-
83
+
86
84
  // Check if there are subtags for the current key
87
85
  if (forwardedTags[key].subtags) {
88
86
  // If subtags exist, add all subtag keys to the result array
89
- Object.keys(forwardedTags[key].subtags).forEach(subkey => {
90
- result.push(subkey);
87
+ Object.keys(forwardedTags[key].subtags).forEach((subkey) => {
88
+ result.push(subkey);
91
89
  });
92
90
  }
93
91
  });
94
92
  return result;
95
- }
93
+ };
96
94
 
97
95
  /**
98
96
  * Extracts the names from the given data.
@@ -155,7 +153,7 @@ export const validateTags = ({
155
153
  unsupportedTags: [],
156
154
  isBraceError: false,
157
155
  };
158
- if(tags && tags.length) {
156
+ if (tags && tags.length) {
159
157
  lodashForEach(tags, ({
160
158
  definition: {
161
159
  supportedModules,
@@ -216,12 +214,12 @@ export const validateTags = ({
216
214
  // validations (eg button) are handled on valid property coming from the response.
217
215
  response.isBraceError ? response.valid = false : response.valid = true;
218
216
  return response;
219
- }
217
+ };
220
218
 
221
219
  /**
222
220
  * Checks if the given tag is supported based on the injected tags.
223
221
  * @param {string} checkingTag - The tag to check.
224
- * @param {Array} injectedTags - The injected tags.
222
+ * @param {Object} injectedTags - The injected tags.
225
223
  * @returns {boolean} - True if the tag is supported, false otherwise.
226
224
  */
227
225
  export const checkIfSupportedTag = (checkingTag, injectedTags) => {
@@ -233,33 +231,27 @@ export const checkIfSupportedTag = (checkingTag, injectedTags) => {
233
231
  result = true;
234
232
  }
235
233
  });
236
-
234
+
237
235
  return result;
238
- }
236
+ };
239
237
 
240
238
  const indexOfEnd = (targetString, string) => {
241
- let io = targetString.indexOf(string);
239
+ const io = targetString.indexOf(string);
242
240
  return io == -1 ? -1 : io + string.length;
243
- }
241
+ };
244
242
 
245
243
  export const skipTags = (tag) => {
246
244
  // If the tag contains the word "entryTrigger.", then it's an event context tag and should not be skipped.
247
245
  if (tag?.match(ENTRY_TRIGGER_TAG_REGEX)) {
248
246
  return false;
249
247
  }
250
- const regexGroups = ["dynamic_expiry_date_after_\\d+_days.FORMAT_\\d", "unsubscribe\\(#[a-zA-Z\\d]{6}\\)","Link_to_[a-zA-z]","SURVEY.*.TOKEN", "^[A-Za-z].*\\([a-zA-Z\\d]*\\)"];
251
- let skipped = false;
252
- lodashForEach(regexGroups, (group) => {
253
- const groupRegex = new RegExp(group, "g");
254
- let match = groupRegex.exec(tag);
255
- if (match !== null ) {
256
- skipped = true;
257
- return true;
258
- }
259
- return true;
248
+ // Use some() to check if any pattern matches (stops on first match)
249
+ return SKIP_TAGS_REGEX_GROUPS.some((group) => {
250
+ // Create a new RegExp for each test to avoid state issues with global flag
251
+ const groupRegex = new RegExp(group);
252
+ return groupRegex.test(tag);
260
253
  });
261
- return skipped;
262
- }
254
+ };
263
255
 
264
256
  export const transformInjectedTags = (tags) => {
265
257
  lodashForEach(tags, (tag) => {
@@ -274,35 +266,100 @@ export const transformInjectedTags = (tags) => {
274
266
  if (subKey !== '') {
275
267
  temp['tag-header'] = true;
276
268
  if (subKey !== SUBTAGS) {
277
- temp.subtags =lodashCloneDeep(temp[subKey]);
269
+ temp.subtags = lodashCloneDeep(temp[subKey]);
278
270
  delete temp[subKey];
279
271
  }
280
272
  temp.subtags = transformInjectedTags(temp.subtags);
281
273
  }
282
274
  });
283
275
  return tags;
284
- }
276
+ };
285
277
 
286
278
  //checks if the opening curly brackets have corresponding closing brackets
287
279
  export const validateIfTagClosed = (value) => {
288
280
  if (value.includes("{{{{") || value.includes("}}}}")) {
289
281
  return false;
290
282
  }
291
- let regex1 = /{{.*?}}/g;
292
- let regex2 = /{{/g;
293
- let regex3 = /}}/g;
283
+ const regex1 = /{{.*?}}/g;
284
+ const regex2 = /{{/g;
285
+ const regex3 = /}}/g;
294
286
 
295
- let l1 = value.match(regex1)?.length;
296
- let l2 = value.match(regex2)?.length;
297
- let l3 = value.match(regex3)?.length;
287
+ const l1 = value.match(regex1)?.length;
288
+ const l2 = value.match(regex2)?.length;
289
+ const l3 = value.match(regex3)?.length;
298
290
 
299
291
  return (l1 == l2 && l2 == l3 && l1 == l3);
300
-
292
+ };
293
+
294
+ /**
295
+ * Validates tag format: ensures tags are in format {{tag_name}} and checks for invalid patterns
296
+ * Validates against:
297
+ * - Single braces like {tag} (must be {{tag}})
298
+ * - Invalid patterns like {{first or first}}, {{first and first}}
299
+ * - Empty tag names
300
+ * - Unclosed single braces within tag names
301
+ * @param {string} textContent - The text content to validate
302
+ * @returns {boolean} - True if all tags have valid format, false otherwise
303
+ */
304
+ export const validateTagFormat = (textContent) => {
305
+ // Find all potential tag patterns {{tag_name}}
306
+ const tagPattern = /{{[^}]*}}/g;
307
+ const matches = textContent.match(tagPattern) || [];
308
+
309
+ // Remove all valid {{tag}} patterns from content to check for invalid braces
310
+ let contentWithoutValidTags = textContent;
311
+ matches.forEach((match) => {
312
+ contentWithoutValidTags = contentWithoutValidTags.replace(match, '');
313
+ });
314
+
315
+ // Check if there are any remaining braces (single braces or unclosed braces)
316
+ // These would be invalid patterns like {tag}, {first, first}, etc.
317
+ if (contentWithoutValidTags.includes('{') || contentWithoutValidTags.includes('}')) {
318
+ return false;
319
+ }
320
+
321
+ // Check each tag for valid format
322
+ const allTagsValid = matches.every((match) => {
323
+ // Valid tag format: {{tag_name}} - must start with {{ and end with }}
324
+ if (!match.startsWith('{{') || !match.endsWith('}}')) {
325
+ return false;
326
+ }
327
+
328
+ // Extract tag name (content between {{ and }})
329
+ const tagName = match.slice(2, -2).trim();
330
+
331
+ // Tag name should not be empty
332
+ if (!tagName) {
333
+ return false;
334
+ }
335
+
336
+ // Check for invalid patterns in tag name
337
+ // Invalid patterns: "first or first", "first and first", etc.
338
+ const invalidPatterns = [
339
+ /\s+or\s+/i, // " or " as separate word (e.g., "first or first")
340
+ /\s+and\s+/i, // " and " as separate word
341
+ ];
342
+
343
+ const hasInvalidPattern = invalidPatterns.some((pattern) => pattern.test(tagName));
344
+ if (hasInvalidPattern) {
345
+ return false;
346
+ }
347
+
348
+ // Check for unclosed single braces in tag name (e.g., {{first{name}})
349
+ const singleOpenBraces = (tagName.match(/{/g) || []).length;
350
+ const singleCloseBraces = (tagName.match(/}/g) || []).length;
351
+ if (singleOpenBraces !== singleCloseBraces) {
352
+ return false;
353
+ }
354
+
355
+ return true;
356
+ });
357
+
358
+ return allTagsValid;
301
359
  };
302
360
 
303
361
  //replaces encoded string with their respective characters
304
362
  export const preprocessHtml = (content) => {
305
-
306
363
  const replacements = {
307
364
  "&#39;": "'",
308
365
  "&quot;": "'",
@@ -310,7 +367,7 @@ export const preprocessHtml = (content) => {
310
367
  "&amp;": "&",
311
368
  "&lt;": "<",
312
369
  "&gt;": ">",
313
- "\n": "", // Handling newlines by replacing them with an empty string
370
+ "\n": "", // Handling newlines by replacing them with an empty string
314
371
  };
315
372
 
316
373
 
@@ -324,28 +381,22 @@ export const preprocessHtml = (content) => {
324
381
  });
325
382
 
326
383
  // Step 2: Perform the standard replacements on the entire content
327
- return contentWithStyleFixes?.replace(/&#39;|&quot;|&amp;|&lt;|&gt;|"|\n/g, match => replacements[match]);
384
+ return contentWithStyleFixes?.replace(/&#39;|&quot;|&amp;|&lt;|&gt;|"|\n/g, (match) => replacements[match]);
328
385
  };
329
386
 
330
387
  //this is used to get the subtags from custom or extended tags
331
- export const getTagMapValue = (object = {}) => {
332
- return Object.values(
333
- object
334
- ).reduce((acc, current) => {
335
- return { ...acc, ...current?.subtags ?? {} };
336
- }, {});
337
- };
338
-
339
- export const getLoyaltyTagsMapValue = (object = {}) => {
340
- return Object.entries(object).reduce((acc, [key, current]) => {
341
- if (current?.subtags && Object.keys(current.subtags).length > 0) {
342
- // If subtags exist → merge them
343
- return { ...acc, ...(current.subtags ?? {}) };
344
- }
345
- // If no subtags → keep the tag itself
346
- return { ...acc, [key]: current };
347
- }, {});
348
- };
388
+ export const getTagMapValue = (object = {}) => Object.values(
389
+ object
390
+ ).reduce((acc, current) => ({ ...acc, ...current?.subtags ?? {} }), {});
391
+
392
+ export const getLoyaltyTagsMapValue = (object = {}) => Object.entries(object).reduce((acc, [key, current]) => {
393
+ if (current?.subtags && Object.keys(current.subtags).length > 0) {
394
+ // If subtags exist → merge them
395
+ return { ...acc, ...(current.subtags ?? {}) };
396
+ }
397
+ // If no subtags keep the tag itself
398
+ return { ...acc, [key]: current };
399
+ }, {});
349
400
 
350
401
 
351
402
  /**
@@ -354,27 +405,25 @@ export const getLoyaltyTagsMapValue = (object = {}) => {
354
405
  * @param {Object} object - The input object containing top-level keys with optional subtags.
355
406
  * @returns {Object} - A flat map containing all top-level keys and their subtags.
356
407
  */
357
- export const getForwardedMapValues = (object = {}) => {
358
- return Object?.entries(object)?.reduce((acc, [key, current]) => {
359
- // Check if current has 'subtags' and it's an object
360
- if (current && current?.subtags && typeof current?.subtags === 'object') {
361
- // Add the top-level key with its 'name' and 'desc'
362
- acc[key] = {
363
- name: current?.name,
364
- desc: current?.desc,
365
- };
366
-
367
- // Merge the subtags into the accumulator
368
- acc = { ...acc, ...current?.subtags };
369
- } else if (current && typeof current === 'object') {
370
- // If no 'subtags', add the top-level key with its 'name' and 'desc'
371
- acc[key] = {
372
- name: current?.name,
373
- desc: current?.desc,
374
- };
375
- }
376
-
377
- // If the current entry is not an object or lacks 'name'/'desc', skip it
378
- return acc;
379
- }, {});
380
- };
408
+ export const getForwardedMapValues = (object = {}) => Object?.entries(object)?.reduce((acc, [key, current]) => {
409
+ // Check if current has 'subtags' and it's an object
410
+ if (current && current?.subtags && typeof current?.subtags === 'object') {
411
+ // Add the top-level key with its 'name' and 'desc'
412
+ acc[key] = {
413
+ name: current?.name,
414
+ desc: current?.desc,
415
+ };
416
+
417
+ // Merge the subtags into the accumulator
418
+ acc = { ...acc, ...current?.subtags };
419
+ } else if (current && typeof current === 'object') {
420
+ // If no 'subtags', add the top-level key with its 'name' and 'desc'
421
+ acc[key] = {
422
+ name: current?.name,
423
+ desc: current?.desc,
424
+ };
425
+ }
426
+
427
+ // If the current entry is not an object or lacks 'name'/'desc', skip it
428
+ return acc;
429
+ }, {});
@@ -7,6 +7,7 @@ import {
7
7
  addBaseToTemplate,
8
8
  validateCarouselCards,
9
9
  } from "../commonUtils";
10
+ import { skipTags } from "../tagValidations";
10
11
  import { SMS_TRAI_VAR } from '../../v2Containers/SmsTrai/Edit/constants';
11
12
  import { ANDROID, IOS } from '../../v2Containers/CreativesContainer/constants';
12
13
 
@@ -122,6 +123,150 @@ describe("validateLiquidTemplateContent", () => {
122
123
  expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
123
124
  });
124
125
 
126
+ it("should skip referral_unique_code tags and not trigger unsupported tag error", async () => {
127
+ const getLiquidTags = jest.fn((content, cb) =>
128
+ cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_C6SOE_userid" }] }, isError: false })
129
+ );
130
+ await validateLiquidTemplateContent("foo", {
131
+ getLiquidTags,
132
+ formatMessage,
133
+ messages,
134
+ onError,
135
+ onSuccess,
136
+ tagLookupMap,
137
+ eventContextTags,
138
+ skipTags
139
+ });
140
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
141
+ expect(onError).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("should skip referral_unique_url tags and not trigger unsupported tag error", async () => {
145
+ const getLiquidTags = jest.fn((content, cb) =>
146
+ cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_url_C6SOE_userid" }] }, isError: false })
147
+ );
148
+ await validateLiquidTemplateContent("foo", {
149
+ getLiquidTags,
150
+ formatMessage,
151
+ messages,
152
+ onError,
153
+ onSuccess,
154
+ tagLookupMap,
155
+ eventContextTags,
156
+ skipTags
157
+ });
158
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
159
+ expect(onError).not.toHaveBeenCalled();
160
+ });
161
+
162
+ it("should skip referral_unique_code tags with different codes", async () => {
163
+ const getLiquidTags = jest.fn((content, cb) =>
164
+ cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_ABC123_userid" }] }, isError: false })
165
+ );
166
+ await validateLiquidTemplateContent("foo", {
167
+ getLiquidTags,
168
+ formatMessage,
169
+ messages,
170
+ onError,
171
+ onSuccess,
172
+ tagLookupMap,
173
+ eventContextTags,
174
+ skipTags
175
+ });
176
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
177
+ expect(onError).not.toHaveBeenCalled();
178
+ });
179
+
180
+ it("should skip referral_unique_url tags with different codes", async () => {
181
+ const getLiquidTags = jest.fn((content, cb) =>
182
+ cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_url_ABC123_userid" }] }, isError: false })
183
+ );
184
+ await validateLiquidTemplateContent("foo", {
185
+ getLiquidTags,
186
+ formatMessage,
187
+ messages,
188
+ onError,
189
+ onSuccess,
190
+ tagLookupMap,
191
+ eventContextTags,
192
+ skipTags
193
+ });
194
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
195
+ expect(onError).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it("should skip referral_unique_code tags with alphanumeric codes", async () => {
199
+ const getLiquidTags = jest.fn((content, cb) =>
200
+ cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_XYZ789_userid" }] }, isError: false })
201
+ );
202
+ await validateLiquidTemplateContent("foo", {
203
+ getLiquidTags,
204
+ formatMessage,
205
+ messages,
206
+ onError,
207
+ onSuccess,
208
+ tagLookupMap,
209
+ eventContextTags,
210
+ skipTags
211
+ });
212
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
213
+ expect(onError).not.toHaveBeenCalled();
214
+ });
215
+
216
+ it("should skip referral_unique_url tags with alphanumeric codes", async () => {
217
+ const getLiquidTags = jest.fn((content, cb) =>
218
+ cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_url_XYZ789_userid" }] }, isError: false })
219
+ );
220
+ await validateLiquidTemplateContent("foo", {
221
+ getLiquidTags,
222
+ formatMessage,
223
+ messages,
224
+ onError,
225
+ onSuccess,
226
+ tagLookupMap,
227
+ eventContextTags,
228
+ skipTags
229
+ });
230
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
231
+ expect(onError).not.toHaveBeenCalled();
232
+ });
233
+
234
+ it("should skip both referral_unique_code and referral_unique_url tags in the same content", async () => {
235
+ const getLiquidTags = jest.fn((content, cb) =>
236
+ cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_C6SOE_userid" }, { name: "referral_unique_url_C6SOE_userid" }] }, isError: false })
237
+ );
238
+ await validateLiquidTemplateContent("foo", {
239
+ getLiquidTags,
240
+ formatMessage,
241
+ messages,
242
+ onError,
243
+ onSuccess,
244
+ tagLookupMap,
245
+ eventContextTags,
246
+ skipTags
247
+ });
248
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
249
+ expect(onError).not.toHaveBeenCalled();
250
+ });
251
+
252
+ it("should skip referral_unique_code_632L7_userid tag (real-world example)", async () => {
253
+ const getLiquidTags = jest.fn((content, cb) =>
254
+ cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_632L7_userid" }] }, isError: false })
255
+ );
256
+ await validateLiquidTemplateContent("foo", {
257
+ getLiquidTags,
258
+ formatMessage,
259
+ messages,
260
+ onError,
261
+ onSuccess,
262
+ tagLookupMap,
263
+ eventContextTags,
264
+ skipTags
265
+ });
266
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
267
+ expect(onError).not.toHaveBeenCalled();
268
+ });
269
+
125
270
  it("calls onError with emailBodyEmptyError when validString is falsy", async () => {
126
271
  const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
127
272
  const formatMessage = jest.fn((msg) => msg.id);