@capillarytech/creatives-library 8.0.224 → 8.0.226

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.224",
4
+ "version": "8.0.226",
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,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).
@@ -39,7 +40,7 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
39
40
  let updatedChildName = childName;
40
41
  let updatedWithoutDotChildName = childName;
41
42
  if (childName?.includes(".")) {
42
- updatedChildName = `.${childName?.split(".")?.[1]}`;
43
+ updatedChildName = "." + childName?.split(".")?.[1];
43
44
  updatedWithoutDotChildName = childName?.split(".")?.[1];
44
45
  }
45
46
  if (tagObject?.[parentTag]) {
@@ -70,6 +71,7 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
70
71
  if (item?.children?.length) {
71
72
  processChildren(item?.name, item?.children);
72
73
  }
74
+
73
75
  }
74
76
 
75
77
 
@@ -78,19 +80,19 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
78
80
 
79
81
  const handleForwardedTags = (forwardedTags) => {
80
82
  const result = [];
81
- Object.keys(forwardedTags).forEach((key) => {
83
+ Object.keys(forwardedTags).forEach(key => {
82
84
  result.push(key); // Add the main key to the result array
83
-
85
+
84
86
  // Check if there are subtags for the current key
85
87
  if (forwardedTags[key].subtags) {
86
88
  // If subtags exist, add all subtag keys to the result array
87
- Object.keys(forwardedTags[key].subtags).forEach((subkey) => {
88
- result.push(subkey);
89
+ Object.keys(forwardedTags[key].subtags).forEach(subkey => {
90
+ result.push(subkey);
89
91
  });
90
92
  }
91
93
  });
92
94
  return result;
93
- };
95
+ }
94
96
 
95
97
  /**
96
98
  * Extracts the names from the given data.
@@ -153,7 +155,7 @@ export const validateTags = ({
153
155
  unsupportedTags: [],
154
156
  isBraceError: false,
155
157
  };
156
- if (tags && tags.length) {
158
+ if(tags && tags.length) {
157
159
  lodashForEach(tags, ({
158
160
  definition: {
159
161
  supportedModules,
@@ -214,12 +216,12 @@ export const validateTags = ({
214
216
  // validations (eg button) are handled on valid property coming from the response.
215
217
  response.isBraceError ? response.valid = false : response.valid = true;
216
218
  return response;
217
- };
219
+ }
218
220
 
219
221
  /**
220
222
  * Checks if the given tag is supported based on the injected tags.
221
223
  * @param {string} checkingTag - The tag to check.
222
- * @param {Array} injectedTags - The injected tags.
224
+ * @param {Object} injectedTags - The injected tags.
223
225
  * @returns {boolean} - True if the tag is supported, false otherwise.
224
226
  */
225
227
  export const checkIfSupportedTag = (checkingTag, injectedTags) => {
@@ -231,32 +233,26 @@ export const checkIfSupportedTag = (checkingTag, injectedTags) => {
231
233
  result = true;
232
234
  }
233
235
  });
234
-
236
+
235
237
  return result;
236
238
  };
237
239
 
238
240
  const indexOfEnd = (targetString, string) => {
239
- const io = targetString.indexOf(string);
241
+ let io = targetString.indexOf(string);
240
242
  return io == -1 ? -1 : io + string.length;
241
- };
243
+ }
242
244
 
243
245
  export const skipTags = (tag) => {
244
246
  // If the tag contains the word "entryTrigger.", then it's an event context tag and should not be skipped.
245
247
  if (tag?.match(ENTRY_TRIGGER_TAG_REGEX)) {
246
248
  return false;
247
249
  }
248
- 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]*\\)"];
249
- let skipped = false;
250
- lodashForEach(regexGroups, (group) => {
251
- const groupRegex = new RegExp(group, "g");
252
- const match = groupRegex.exec(tag);
253
- if (match !== null ) {
254
- skipped = true;
255
- return true;
256
- }
257
- return true;
250
+ // Use some() to check if any pattern matches (stops on first match)
251
+ return SKIP_TAGS_REGEX_GROUPS.some((group) => {
252
+ // Create a new RegExp for each test to avoid state issues with global flag
253
+ const groupRegex = new RegExp(group);
254
+ return groupRegex.test(tag);
258
255
  });
259
- return skipped;
260
256
  };
261
257
 
262
258
  export const transformInjectedTags = (tags) => {
@@ -272,100 +268,35 @@ export const transformInjectedTags = (tags) => {
272
268
  if (subKey !== '') {
273
269
  temp['tag-header'] = true;
274
270
  if (subKey !== SUBTAGS) {
275
- temp.subtags = lodashCloneDeep(temp[subKey]);
271
+ temp.subtags =lodashCloneDeep(temp[subKey]);
276
272
  delete temp[subKey];
277
273
  }
278
274
  temp.subtags = transformInjectedTags(temp.subtags);
279
275
  }
280
276
  });
281
277
  return tags;
282
- };
278
+ }
283
279
 
284
280
  //checks if the opening curly brackets have corresponding closing brackets
285
281
  export const validateIfTagClosed = (value) => {
286
282
  if (value.includes("{{{{") || value.includes("}}}}")) {
287
283
  return false;
288
284
  }
289
- const regex1 = /{{.*?}}/g;
290
- const regex2 = /{{/g;
291
- const regex3 = /}}/g;
285
+ let regex1 = /{{.*?}}/g;
286
+ let regex2 = /{{/g;
287
+ let regex3 = /}}/g;
292
288
 
293
- const l1 = value.match(regex1)?.length;
294
- const l2 = value.match(regex2)?.length;
295
- const l3 = value.match(regex3)?.length;
289
+ let l1 = value.match(regex1)?.length;
290
+ let l2 = value.match(regex2)?.length;
291
+ let l3 = value.match(regex3)?.length;
296
292
 
297
293
  return (l1 == l2 && l2 == l3 && l1 == l3);
298
- };
299
-
300
- /**
301
- * Validates tag format: ensures tags are in format {{tag_name}} and checks for invalid patterns
302
- * Validates against:
303
- * - Single braces like {tag} (must be {{tag}})
304
- * - Invalid patterns like {{first or first}}, {{first and first}}
305
- * - Empty tag names
306
- * - Unclosed single braces within tag names
307
- * @param {string} textContent - The text content to validate
308
- * @returns {boolean} - True if all tags have valid format, false otherwise
309
- */
310
- export const validateTagFormat = (textContent) => {
311
- // Find all potential tag patterns {{tag_name}}
312
- const tagPattern = /{{[^}]*}}/g;
313
- const matches = textContent.match(tagPattern) || [];
314
-
315
- // Remove all valid {{tag}} patterns from content to check for invalid braces
316
- let contentWithoutValidTags = textContent;
317
- matches.forEach((match) => {
318
- contentWithoutValidTags = contentWithoutValidTags.replace(match, '');
319
- });
320
-
321
- // Check if there are any remaining braces (single braces or unclosed braces)
322
- // These would be invalid patterns like {tag}, {first, first}, etc.
323
- if (contentWithoutValidTags.includes('{') || contentWithoutValidTags.includes('}')) {
324
- return false;
325
- }
326
-
327
- // Check each tag for valid format
328
- const allTagsValid = matches.every((match) => {
329
- // Valid tag format: {{tag_name}} - must start with {{ and end with }}
330
- if (!match.startsWith('{{') || !match.endsWith('}}')) {
331
- return false;
332
- }
333
-
334
- // Extract tag name (content between {{ and }})
335
- const tagName = match.slice(2, -2).trim();
336
-
337
- // Tag name should not be empty
338
- if (!tagName) {
339
- return false;
340
- }
341
-
342
- // Check for invalid patterns in tag name
343
- // Invalid patterns: "first or first", "first and first", etc.
344
- const invalidPatterns = [
345
- /\s+or\s+/i, // " or " as separate word (e.g., "first or first")
346
- /\s+and\s+/i, // " and " as separate word
347
- ];
348
-
349
- const hasInvalidPattern = invalidPatterns.some((pattern) => pattern.test(tagName));
350
- if (hasInvalidPattern) {
351
- return false;
352
- }
353
-
354
- // Check for unclosed single braces in tag name (e.g., {{first{name}})
355
- const singleOpenBraces = (tagName.match(/{/g) || []).length;
356
- const singleCloseBraces = (tagName.match(/}/g) || []).length;
357
- if (singleOpenBraces !== singleCloseBraces) {
358
- return false;
359
- }
360
-
361
- return true;
362
- });
363
-
364
- return allTagsValid;
294
+
365
295
  };
366
296
 
367
297
  //replaces encoded string with their respective characters
368
298
  export const preprocessHtml = (content) => {
299
+
369
300
  const replacements = {
370
301
  "&#39;": "'",
371
302
  "&quot;": "'",
@@ -373,7 +304,7 @@ export const preprocessHtml = (content) => {
373
304
  "&amp;": "&",
374
305
  "&lt;": "<",
375
306
  "&gt;": ">",
376
- "\n": "", // Handling newlines by replacing them with an empty string
307
+ "\n": "", // Handling newlines by replacing them with an empty string
377
308
  };
378
309
 
379
310
 
@@ -387,22 +318,28 @@ export const preprocessHtml = (content) => {
387
318
  });
388
319
 
389
320
  // Step 2: Perform the standard replacements on the entire content
390
- return contentWithStyleFixes?.replace(/&#39;|&quot;|&amp;|&lt;|&gt;|"|\n/g, (match) => replacements[match]);
321
+ return contentWithStyleFixes?.replace(/&#39;|&quot;|&amp;|&lt;|&gt;|"|\n/g, match => replacements[match]);
391
322
  };
392
323
 
393
324
  //this is used to get the subtags from custom or extended tags
394
- export const getTagMapValue = (object = {}) => Object.values(
395
- object
396
- ).reduce((acc, current) => ({ ...acc, ...current?.subtags ?? {} }), {});
397
-
398
- export const getLoyaltyTagsMapValue = (object = {}) => Object.entries(object).reduce((acc, [key, current]) => {
399
- if (current?.subtags && Object.keys(current.subtags).length > 0) {
400
- // If subtags exist → merge them
401
- return { ...acc, ...(current.subtags ?? {}) };
402
- }
403
- // If no subtags keep the tag itself
404
- return { ...acc, [key]: current };
405
- }, {});
325
+ export const getTagMapValue = (object = {}) => {
326
+ return Object.values(
327
+ object
328
+ ).reduce((acc, current) => {
329
+ return { ...acc, ...current?.subtags ?? {} };
330
+ }, {});
331
+ };
332
+
333
+ export const getLoyaltyTagsMapValue = (object = {}) => {
334
+ return Object.entries(object).reduce((acc, [key, current]) => {
335
+ if (current?.subtags && Object.keys(current.subtags).length > 0) {
336
+ // If subtags exist → merge them
337
+ return { ...acc, ...(current.subtags ?? {}) };
338
+ }
339
+ // If no subtags → keep the tag itself
340
+ return { ...acc, [key]: current };
341
+ }, {});
342
+ };
406
343
 
407
344
 
408
345
  /**
@@ -411,25 +348,27 @@ export const getLoyaltyTagsMapValue = (object = {}) => Object.entries(object).re
411
348
  * @param {Object} object - The input object containing top-level keys with optional subtags.
412
349
  * @returns {Object} - A flat map containing all top-level keys and their subtags.
413
350
  */
414
- export const getForwardedMapValues = (object = {}) => Object?.entries(object)?.reduce((acc, [key, current]) => {
415
- // Check if current has 'subtags' and it's an object
416
- if (current && current?.subtags && typeof current?.subtags === 'object') {
417
- // Add the top-level key with its 'name' and 'desc'
418
- acc[key] = {
419
- name: current?.name,
420
- desc: current?.desc,
421
- };
422
-
423
- // Merge the subtags into the accumulator
424
- acc = { ...acc, ...current?.subtags };
425
- } else if (current && typeof current === 'object') {
426
- // If no 'subtags', add the top-level key with its 'name' and 'desc'
427
- acc[key] = {
428
- name: current?.name,
429
- desc: current?.desc,
430
- };
431
- }
432
-
433
- // If the current entry is not an object or lacks 'name'/'desc', skip it
434
- return acc;
435
- }, {});
351
+ export const getForwardedMapValues = (object = {}) => {
352
+ return Object?.entries(object)?.reduce((acc, [key, current]) => {
353
+ // Check if current has 'subtags' and it's an object
354
+ if (current && current?.subtags && typeof current?.subtags === 'object') {
355
+ // Add the top-level key with its 'name' and 'desc'
356
+ acc[key] = {
357
+ name: current?.name,
358
+ desc: current?.desc,
359
+ };
360
+
361
+ // Merge the subtags into the accumulator
362
+ acc = { ...acc, ...current?.subtags };
363
+ } else if (current && typeof current === 'object') {
364
+ // If no 'subtags', add the top-level key with its 'name' and 'desc'
365
+ acc[key] = {
366
+ name: current?.name,
367
+ desc: current?.desc,
368
+ };
369
+ }
370
+
371
+ // If the current entry is not an object or lacks 'name'/'desc', skip it
372
+ return acc;
373
+ }, {});
374
+ };
@@ -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);
@@ -952,6 +952,92 @@ describe("skipTags", () => {
952
952
  const result = skipTags(tag);
953
953
  expect(result).toEqual(true);
954
954
  });
955
+
956
+ it("should return true for referral unique code tags with userid", () => {
957
+ const tag = "referral_unique_code_C6SOE_userid";
958
+ const result = skipTags(tag);
959
+ expect(result).toEqual(true);
960
+ });
961
+
962
+ it("should return true for referral unique url tags with userid", () => {
963
+ const tag = "referral_unique_url_C6SOE_userid";
964
+ const result = skipTags(tag);
965
+ expect(result).toEqual(true);
966
+ });
967
+
968
+ it("should return true for referral unique code tags with different codes", () => {
969
+ const tag = "referral_unique_code_ABC123_userid";
970
+ const result = skipTags(tag);
971
+ expect(result).toEqual(true);
972
+ });
973
+
974
+ it("should return true for referral unique url tags with different codes", () => {
975
+ const tag = "referral_unique_url_ABC123_userid";
976
+ const result = skipTags(tag);
977
+ expect(result).toEqual(true);
978
+ });
979
+
980
+ it("should return true for referral unique code tags with alphanumeric codes", () => {
981
+ const tag = "referral_unique_code_XYZ789_userid";
982
+ const result = skipTags(tag);
983
+ expect(result).toEqual(true);
984
+ });
985
+
986
+ it("should return true for referral unique url tags with alphanumeric codes", () => {
987
+ const tag = "referral_unique_url_XYZ789_userid";
988
+ const result = skipTags(tag);
989
+ expect(result).toEqual(true);
990
+ });
991
+
992
+ it("should return false for tags that don't match referral pattern", () => {
993
+ const tag = "referral_code_userid";
994
+ const result = skipTags(tag);
995
+ expect(result).toEqual(false);
996
+ });
997
+
998
+ it("should return false for tags with referral but missing unique_code or unique_url", () => {
999
+ const tag = "referral_C6SOE_userid";
1000
+ const result = skipTags(tag);
1001
+ expect(result).toEqual(false);
1002
+ });
1003
+
1004
+ it("should return false for tags with referral_unique_code but missing userid", () => {
1005
+ const tag = "referral_unique_code_C6SOE";
1006
+ const result = skipTags(tag);
1007
+ expect(result).toEqual(false);
1008
+ });
1009
+
1010
+ it("should return false for tags with referral_unique_url but missing userid", () => {
1011
+ const tag = "referral_unique_url_C6SOE";
1012
+ const result = skipTags(tag);
1013
+ expect(result).toEqual(false);
1014
+ });
1015
+
1016
+ it("should return true for referral_unique_code tags with numeric codes like 632L7", () => {
1017
+ const tag = "referral_unique_code_632L7_userid";
1018
+ const result = skipTags(tag);
1019
+ expect(result).toEqual(true);
1020
+ });
1021
+
1022
+ it("should return true for referral_unique_url tags with numeric codes like 632L7", () => {
1023
+ const tag = "referral_unique_url_632L7_userid";
1024
+ const result = skipTags(tag);
1025
+ expect(result).toEqual(true);
1026
+ });
1027
+
1028
+ it("should return true for referral tags with various token formats", () => {
1029
+ const tags = [
1030
+ "referral_unique_code_123_userid",
1031
+ "referral_unique_code_ABC_userid",
1032
+ "referral_unique_code_123ABC_userid",
1033
+ "referral_unique_url_456_userid",
1034
+ "referral_unique_url_DEF_userid",
1035
+ "referral_unique_url_456DEF_userid",
1036
+ ];
1037
+ tags.forEach((tag) => {
1038
+ expect(skipTags(tag)).toEqual(true);
1039
+ });
1040
+ });
955
1041
  });
956
1042
 
957
1043
 
@@ -57,7 +57,7 @@ import { convert } from 'html-to-text';
57
57
  import { OUTBOUND, ADD_LANGUAGE, UPLOAD, USE_EDITOR, COPY_PRIMARY_LANGUAGE } from './constants';
58
58
  import { GET_TRANSLATION_MAPPED } from '../../constants/unified';
59
59
  import moment from 'moment';
60
- import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX} from '../../constants/unified';
60
+ import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS} from '../../constants/unified';
61
61
  import { REQUEST } from '../../v2Containers/Cap/constants'
62
62
  import { hasLiquidSupportFeature, isEmailUnsubscribeTagMandatory } from '../../utils/common';
63
63
  import { isUrl } from '../../v2Containers/Line/Container/Wrapper/utils';
@@ -1491,20 +1491,12 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1491
1491
  if (tag?.match(ENTRY_TRIGGER_TAG_REGEX)) {
1492
1492
  return false;
1493
1493
  }
1494
- 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]*\\)"];
1495
- //const regexGroups = [];
1496
- let skipped = false;
1497
- _.forEach(regexGroups, (group) => {
1498
- //const groupRegex = /dynamic_expiry_date_after_\(\d+\)_days.FORMAT_\d/g;
1499
- const groupRegex = new RegExp(group, "g");
1500
- let match = groupRegex.exec(tag);
1501
- if (match !== null ) {
1502
- skipped = true;
1503
- return true;
1504
- }
1505
- return true;
1494
+ // Use some() to check if any pattern matches (stops on first match)
1495
+ return SKIP_TAGS_REGEX_GROUPS.some((group) => {
1496
+ // Create a new RegExp for each test to avoid state issues with global flag
1497
+ const groupRegex = new RegExp(group);
1498
+ return groupRegex.test(tag);
1506
1499
  });
1507
- return skipped;
1508
1500
  }
1509
1501
 
1510
1502
  validateTags(content, tagsParam, injectedTagsParams, isEmail = false) {
@@ -19,7 +19,6 @@ const SendTestMessage = ({
19
19
  formData,
20
20
  isSendingTestMessage,
21
21
  formatMessage,
22
- isContentValid = true,
23
22
  }) => (
24
23
  <CapStepsAccordian
25
24
  showNumberSteps={false}
@@ -44,11 +43,7 @@ const SendTestMessage = ({
44
43
  multiple
45
44
  placeholder={formatMessage(messages.testCustomersPlaceholder)}
46
45
  />
47
- <CapButton
48
- onClick={handleSendTestMessage}
49
- disabled={isEmpty(selectedTestEntities) || (isEmpty(formData['template-subject']) && isEmpty(formData[0]?.['template-subject'])) || isSendingTestMessage || !isContentValid}
50
- title={!isContentValid ? formatMessage(messages.contentInvalid) : ''}
51
- >
46
+ <CapButton onClick={handleSendTestMessage} disabled={isEmpty(selectedTestEntities) || (isEmpty(formData['template-subject']) && isEmpty(formData[0]?.['template-subject'])) || isSendingTestMessage}>
52
47
  <FormattedMessage {...messages.sendTestButton} />
53
48
  </CapButton>
54
49
  </CapRow>),
@@ -68,7 +63,6 @@ SendTestMessage.propTypes = {
68
63
  formData: PropTypes.object.isRequired,
69
64
  isSendingTestMessage: PropTypes.bool.isRequired,
70
65
  formatMessage: PropTypes.func.isRequired,
71
- isContentValid: PropTypes.bool,
72
66
  };
73
67
 
74
68
  export default SendTestMessage;
@@ -52,7 +52,6 @@ import {
52
52
  INITIAL_PAYLOAD, EMAIL, TEST, DESKTOP, ACTIVE, MOBILE,
53
53
  } from './constants';
54
54
  import { GLOBAL_CONVERT_OPTIONS } from '../FormBuilder/constants';
55
- import { validateIfTagClosed, validateTagFormat } from '../../utils/tagValidations';
56
55
 
57
56
  const TestAndPreviewSlidebox = (props) => {
58
57
  const {
@@ -104,58 +103,10 @@ const TestAndPreviewSlidebox = (props) => {
104
103
  const [selectedTestEntities, setSelectedTestEntities] = useState([]);
105
104
  const [beeContent, setBeeContent] = useState(''); // Track BEE editor content separately
106
105
  const previousBeeContentRef = useRef(''); // Track previous BEE content to prevent unnecessary updates
107
- const [isContentValid, setIsContentValid] = useState(true); // Track if content tags are valid
108
106
 
109
107
  const isUpdatePreviewDisabled = useMemo(() => (
110
- requiredTags.some((tag) => !customValues[tag.fullPath]) || !isContentValid
111
- ), [requiredTags, customValues, isContentValid]);
112
-
113
- /**
114
- * Validates tags in content: checks for proper tag format and balanced braces
115
- * Uses validateIfTagClosed and validateTagFormat from utils/tagValidations
116
- * @param {string} content - The HTML content to validate
117
- * @returns {boolean} - True if content is valid, false otherwise
118
- */
119
- const validateContentTags = (content) => {
120
- if (!content) return true;
121
-
122
- try {
123
- // Convert HTML to text (same as what's used for tag extraction)
124
- // This ensures we validate the same content that will be used for tag extraction
125
- const textContent = convert(content, GLOBAL_CONVERT_OPTIONS);
126
-
127
- // Check if there are any braces in the content
128
- const hasBraces = textContent.includes('{') || textContent.includes('}');
129
-
130
- // If no braces exist, content is valid (no tag validation needed)
131
- if (!hasBraces) {
132
- return true;
133
- }
134
-
135
- // First check if tags are properly closed using the utility function from tagValidations
136
- // This validates that all opening braces have corresponding closing braces
137
- if (!validateIfTagClosed(textContent)) {
138
- return false;
139
- }
140
-
141
- // Now validate tag format: tags must be in format {{tag_name}}
142
- return validateTagFormat(textContent);
143
- } catch (error) {
144
- // If conversion fails, fall back to validating the original content
145
- console.warn('Error converting content for validation:', error);
146
- const hasBraces = content.includes('{') || content.includes('}');
147
- if (!hasBraces) {
148
- return true;
149
- }
150
-
151
- // Apply same validation to original content
152
- if (!validateIfTagClosed(content)) {
153
- return false;
154
- }
155
-
156
- return validateTagFormat(content);
157
- }
158
- };
108
+ requiredTags.some((tag) => !customValues[tag.fullPath])
109
+ ), [requiredTags, customValues]);
159
110
 
160
111
  // Function to resolve tags in text with custom values
161
112
  const resolveTagsInText = (text, tagValues) => {
@@ -202,10 +153,6 @@ const TestAndPreviewSlidebox = (props) => {
202
153
  if (existingContent && existingContent.trim() !== '') {
203
154
  // We already have content, update local state only if it's different
204
155
  if (existingContent !== previousBeeContentRef.current) {
205
- // Validate content tags for BEE editor
206
- const isValid = validateContentTags(existingContent);
207
- setIsContentValid(isValid);
208
-
209
156
  previousBeeContentRef.current = existingContent;
210
157
  setBeeContent(existingContent);
211
158
  setPreviewDataHtml({
@@ -239,10 +186,6 @@ const TestAndPreviewSlidebox = (props) => {
239
186
  }
240
187
 
241
188
  if (htmlFile) {
242
- // Validate content tags
243
- const isValid = validateContentTags(htmlFile);
244
- setIsContentValid(isValid);
245
-
246
189
  // Update our states
247
190
  previousBeeContentRef.current = htmlFile;
248
191
  setBeeContent(htmlFile);
@@ -251,16 +194,9 @@ const TestAndPreviewSlidebox = (props) => {
251
194
  resolvedTitle: formData['template-subject'] || ''
252
195
  });
253
196
 
254
- // Only extract tags if content is valid
255
- if (isValid) {
256
- const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
257
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
258
- } else {
259
- // Show error notification for invalid content
260
- CapNotification.error({
261
- message: formatMessage(messages.contentInvalid),
262
- });
263
- }
197
+ // Always extract tags when content changes
198
+ const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
199
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
264
200
  }
265
201
 
266
202
  // Restore original handler
@@ -275,45 +211,23 @@ const TestAndPreviewSlidebox = (props) => {
275
211
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
276
212
 
277
213
  if (templateContent) {
278
- // Validate content tags
279
- const isValid = validateContentTags(templateContent);
280
- setIsContentValid(isValid);
281
-
282
214
  // Update preview with initial content
283
215
  setPreviewDataHtml({
284
216
  resolvedBody: templateContent,
285
217
  resolvedTitle: formData['template-subject'] || ''
286
218
  });
287
219
 
288
- // Only extract tags if content is valid
289
- if (isValid) {
290
- const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
291
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
292
- } else {
293
- // Show error notification for invalid content
294
- CapNotification.error({
295
- message: formatMessage(messages.contentInvalid),
296
- });
297
- }
220
+ // Always extract tags when showing
221
+ const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
222
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
298
223
  } else {
299
224
  // Fallback to content prop if no template content
300
- const contentToValidate = getCurrentContent;
301
- const isValid = validateContentTags(contentToValidate);
302
- setIsContentValid(isValid);
303
-
304
- // Only extract tags if content is valid
305
- if (isValid) {
306
- const payloadContent = convert(
307
- contentToValidate,
308
- GLOBAL_CONVERT_OPTIONS
309
- );
310
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
311
- } else {
312
- // Show error notification for invalid content
313
- CapNotification.error({
314
- message: formatMessage(messages.contentInvalid),
315
- });
316
- }
225
+ const payloadContent = convert(
226
+ getCurrentContent,
227
+ GLOBAL_CONVERT_OPTIONS
228
+ );
229
+ // Always extract tags when showing
230
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
317
231
  }
318
232
  }
319
233
 
@@ -329,28 +243,17 @@ const TestAndPreviewSlidebox = (props) => {
329
243
  const isDragDrop = currentTabData?.[activeTab]?.is_drag_drop;
330
244
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
331
245
 
332
- if (templateContent && templateContent.trim() !== '' && show) {
333
- // Common function to handle content update with validation
246
+ if (templateContent && templateContent.trim() !== '') {
247
+ // Common function to handle content update
334
248
  const handleContentUpdate = (content) => {
335
- // Validate content tags for each update
336
- const isValid = validateContentTags(content);
337
- setIsContentValid(isValid);
338
-
339
249
  setPreviewDataHtml({
340
250
  resolvedBody: content,
341
251
  resolvedTitle: formData['template-subject'] || ''
342
252
  });
343
253
 
344
- // Only extract tags if content is valid
345
- if (isValid) {
346
- const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
347
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
348
- } else {
349
- // Show error notification for invalid content
350
- CapNotification.error({
351
- message: formatMessage(messages.contentInvalid),
352
- });
353
- }
254
+ // Extract tags from content
255
+ const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
256
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
354
257
  };
355
258
 
356
259
  if (isDragDrop) {
@@ -384,7 +287,6 @@ const TestAndPreviewSlidebox = (props) => {
384
287
  setTagsExtracted(false);
385
288
  setPreviewDevice('desktop');
386
289
  setSelectedTestEntities([]);
387
- setIsContentValid(true);
388
290
  actions.clearPrefilledValues();
389
291
  }
390
292
  }, [show]);
@@ -628,22 +530,6 @@ const TestAndPreviewSlidebox = (props) => {
628
530
 
629
531
  // Handle update preview
630
532
  const handleUpdatePreview = async () => {
631
- // Re-validate content to get latest state (in case liquid errors were fixed)
632
- const currentTabData = formData[currentTab - 1];
633
- const activeTab = currentTabData?.activeTab;
634
- const templateContent = currentTabData?.[activeTab]?.['template-content'];
635
- const contentToValidate = templateContent || getCurrentContent;
636
- const isValid = validateContentTags(contentToValidate);
637
- setIsContentValid(isValid);
638
-
639
- // Check if content is valid before updating preview
640
- if (!isValid) {
641
- CapNotification.error({
642
- message: formatMessage(messages.contentInvalid),
643
- });
644
- return;
645
- }
646
-
647
533
  try {
648
534
  // Include unsubscribe tag if content contains it
649
535
  const resolvedTags = { ...customValues };
@@ -673,20 +559,9 @@ const TestAndPreviewSlidebox = (props) => {
673
559
  const currentTabData = formData[currentTab - 1];
674
560
  const activeTab = currentTabData?.activeTab;
675
561
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
676
- const content = templateContent || getCurrentContent;
677
-
678
- // Validate content tags before extracting
679
- const isValid = validateContentTags(content);
680
- setIsContentValid(isValid);
681
-
682
- if (!isValid) {
683
- CapNotification.error({
684
- message: formatMessage(messages.contentInvalid),
685
- });
686
- return;
687
- }
688
562
 
689
563
  // Check for personalization tags (excluding unsubscribe)
564
+ const content = templateContent || getCurrentContent;
690
565
  const tags = content.match(/{{[^}]+}}/g) || [];
691
566
  const hasPersonalizationTags = tags.some(tag => !tag.includes('unsubscribe'));
692
567
 
@@ -715,22 +590,6 @@ const TestAndPreviewSlidebox = (props) => {
715
590
  };
716
591
 
717
592
  const handleSendTestMessage = () => {
718
- // Re-validate content to get latest state (in case liquid errors were fixed)
719
- const currentTabData = formData[currentTab - 1];
720
- const activeTab = currentTabData?.activeTab;
721
- const templateContent = currentTabData?.[activeTab]?.['template-content'];
722
- const contentToValidate = templateContent || getCurrentContent;
723
- const isValid = validateContentTags(contentToValidate);
724
- setIsContentValid(isValid);
725
-
726
- // Check if content is valid before sending test message
727
- if (!isValid) {
728
- CapNotification.error({
729
- message: formatMessage(messages.contentInvalid),
730
- });
731
- return;
732
- }
733
-
734
593
  const allUserIds = [];
735
594
  selectedTestEntities.forEach((entityId) => {
736
595
  const group = testGroups.find((g) => g.groupId === entityId);
@@ -826,7 +685,6 @@ const TestAndPreviewSlidebox = (props) => {
826
685
  formData={formData}
827
686
  isSendingTestMessage={isSendingTestMessage}
828
687
  formatMessage={formatMessage}
829
- isContentValid={isContentValid}
830
688
  />
831
689
  );
832
690
 
@@ -144,12 +144,4 @@ export default defineMessages({
144
144
  id: `${scope}.invalidJSON`,
145
145
  defaultMessage: 'Invalid JSON input',
146
146
  },
147
- contentInvalid: {
148
- id: `${scope}.contentInvalid`,
149
- defaultMessage: 'Content is invalid. Please fix the tags in your content before testing or previewing.',
150
- },
151
- previewUpdateError: {
152
- id: `${scope}.previewUpdateError`,
153
- defaultMessage: 'Failed to update preview',
154
- },
155
147
  });