@capillarytech/creatives-library 8.0.285-alpha.0 → 8.0.285-alpha.1

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.
package/initialState.js CHANGED
@@ -14,9 +14,7 @@ export default {
14
14
  metaEntities: {
15
15
  tags: [],
16
16
  layouts: [],
17
- tagLookupMap: {},
18
17
  },
19
- liquidTags: [],
20
18
  fetchingLiquidTags: false,
21
19
  fetchingSchema: true,
22
20
  fetchingSchemaError: '',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.285-alpha.0",
4
+ "version": "8.0.285-alpha.1",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -125,7 +125,11 @@ export const hasCustomerBarcodeFeatureEnabled = Auth.hasFeatureAccess.bind(
125
125
  ENABLE_CUSTOMER_BARCODE_TAG,
126
126
  );
127
127
 
128
- export const isEmailUnsubscribeTagMandatory = Auth.hasFeatureAccess.bind(
128
+ // Note: The "EMAIL_UNSUBSCRIBE_TAG_MANDATORY" feature flag determines if the Unsubscribe tag in email is optional.
129
+ // When this flag is enabled for an org, the Unsubscribe tag is NOT mandatory in the email flow.
130
+ // This is as per the requirement in the tech doc:
131
+ // https://capillarytech.atlassian.net/wiki/spaces/CAM/pages/3941662838/Remove+mandate+for+Unsubscribe+tag+in+email+flow
132
+ export const isEmailUnsubscribeTagOptional = Auth.hasFeatureAccess.bind(
129
133
  null,
130
134
  EMAIL_UNSUBSCRIBE_TAG_MANDATORY,
131
135
  );
@@ -140,10 +140,6 @@ export const validateLiquidTemplateContent = async (
140
140
  messages,
141
141
  onError = () => {},
142
142
  onSuccess = () => {},
143
- tagLookupMap,
144
- eventContextTags,
145
- isLiquidFlow,
146
- forwardedTags = {},
147
143
  tabType,
148
144
  } = options;
149
145
  const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
@@ -288,7 +284,7 @@ export const _validatePlatformSpecificContent = async (
288
284
  /**
289
285
  * Validate Mobile Push content for both Android and iOS tabs
290
286
  * @param {object} formData - Form data containing Android and iOS content
291
- * @param {object} options - Options for validation (currentTab, onError, onSuccess, getLiquidTags, formatMessage, messages, tagLookupMap, eventContextTags, isLiquidFlow, forwardedTags, singleTab). skipTags and extractNames are no longer accepted.
287
+ * @param {object} options - Options for validation (currentTab, onError, onSuccess, getLiquidTags, formatMessage, messages, singleTab).
292
288
  * @returns {Promise<boolean>} - Promise that resolves to true when validation succeeds, false otherwise
293
289
  */
294
290
  export const validateMobilePushContent = async (formData, options) => {
@@ -13,168 +13,10 @@ import { ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS } from '../constants/un
13
13
  const DEFAULT = 'default';
14
14
  const SUBTAGS = 'subtags';
15
15
 
16
- /**
17
- * Checks if the response object of Tags supports the Tags added in the Add Labels Section
18
- * @param {Object} response - The response object to check.
19
- * @param {Object} tagObject - The tagLookupMap.
20
- */
21
- export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [], isLiquidFlow = false, forwardedTags = {}) => {
22
- const supportedList = [];
23
- // Verifies the presence of the tag in the 'Add Labels' section.
24
- // Incase of journey event context the tags won't be available in the tagObject(tagLookupMap).
25
- //Here forwardedTags only use in case of loyalty module
26
- const mappedForwardedTags = handleForwardedTags(forwardedTags);
27
- const checkNameInTagObjectOrEventContext = (name) => !!tagObject[name] || eventContextTags?.some((tag) => tag?.tagName === name) || mappedForwardedTags.includes(name);
28
-
29
- // Verify if childTag is a valid sub-tag of parentTag from the 'Add Labels' section or if it's unsupported.
30
- const checkSubtags = (parentTag, childName) => {
31
- // For event context tags the parentTag will be the event context tag name and subtags will be the child attributes for leaderboards
32
- if (checkNameInTagObjectOrEventContext(parentTag) && isLiquidFlow && eventContextTags?.length) {
33
- const childNameAfterDot = childName?.split(".")?.[1];
34
- if (eventContextTags?.some((tag) => tag?.subTags?.includes(childNameAfterDot))) {
35
- supportedList.push(childName);
36
- }
37
- }
38
- if (!tagObject?.[parentTag] && !mappedForwardedTags.includes(parentTag)) return false;
39
- let updatedChildName = childName;
40
- let updatedWithoutDotChildName = childName;
41
- if (childName?.includes(".")) {
42
- updatedChildName = `.${childName?.split(".")?.[1]}`;
43
- updatedWithoutDotChildName = childName?.split(".")?.[1];
44
- }
45
- if (tagObject?.[parentTag]) {
46
- const subTags = tagObject?.[parentTag]?.definition?.subtags;
47
- return subTags?.includes(updatedChildName);
48
- }
49
- return mappedForwardedTags.includes(updatedChildName) || mappedForwardedTags.includes(updatedWithoutDotChildName) || mappedForwardedTags.includes(childName);
50
- };
51
-
52
- //Recursively checks if the childTag is actually a Sub-tag of the ParentTag
53
- const processChildren = (parentTag = "", children = []) => {
54
- for (const child of children) {
55
- if (checkSubtags(parentTag, child?.name)) {
56
- supportedList.push(child?.name);
57
- }
58
- if (child?.children?.length) {
59
- processChildren(child?.name, child?.children);
60
- }
61
- }
62
- };
63
-
64
- //Checks if the tag is present in the Add Label Section
65
- for (const item of response?.data || []) {
66
- if (checkNameInTagObjectOrEventContext(item?.name)) {
67
- supportedList?.push(item?.name);
68
- }
69
- //Repeat the process for subtags
70
- if (item?.children?.length) {
71
- processChildren(item?.name, item?.children);
72
- }
73
- }
74
-
75
-
76
- return supportedList;
77
- };
78
-
79
- const handleForwardedTags = (forwardedTags) => {
80
- const result = [];
81
- Object.keys(forwardedTags).forEach((key) => {
82
- result.push(key); // Add the main key to the result array
83
-
84
- // Check if there are subtags for the current key
85
- if (forwardedTags[key].subtags) {
86
- // If subtags exist, add all subtag keys to the result array
87
- Object.keys(forwardedTags[key].subtags).forEach((subkey) => {
88
- result.push(subkey);
89
- });
90
- }
91
- });
92
- return result;
93
- };
94
-
95
- /**
96
- * Extracts the names from the given data.
97
- * @param {Array} data - The data to extract names from.
98
- * @returns {Array} - The extracted names.
99
- */
100
- export function extractNames(data) {
101
- const names = [];
102
-
103
- function traverse(node) {
104
- if (node?.name) {
105
- names.push(node?.name);
106
- }
107
-
108
- if (node?.children?.length > 0) {
109
- node?.children?.forEach((child) => traverse(child));
110
- }
111
- }
112
-
113
- data?.forEach((item) => traverse(item));
114
-
115
- return names;
116
- }
117
-
118
- // Helper to check if a tag is inside a {% ... %} block
119
- // Handles all Liquid block types: {% %}, {%- -%}, {%- %}, {% -%}
120
- // Content inside {% %} blocks can contain any dynamic tags and should not be validated
121
- export const isInsideLiquidBlock = (content, tagIndex) => {
122
- if (!content || tagIndex < 0 || tagIndex >= content.length) {
123
- return false;
124
- }
125
-
126
- // Check if tagIndex is at the start of a {% block
127
- // Need to check a few characters ahead to catch {% patterns
128
- const checkWindow = content.substring(Math.max(0, tagIndex - 1), Math.min(content.length, tagIndex + 3));
129
- if (/{%-?/.test(checkWindow) && content.substring(tagIndex, tagIndex + 2) === '{%') {
130
- return true;
131
- }
132
-
133
- // Check content up to tagIndex to see if we're inside any {% %} block
134
- // We need to properly handle Liquid tags:
135
- // - Block opening tags: {% for %}, {% if %}, {% unless %}, {% case %}, {% capture %}, etc.
136
- // These OPEN a block that needs to be closed with an "end" tag
137
- // - Block closing tags: {% endfor %}, {% endif %}, {% endunless %}, etc.
138
- // These CLOSE a block opened by a corresponding opening tag
139
- // - Self-contained tags: {% assign %}, {% comment %}, etc.
140
- // These are blocks themselves (from {% to %})
141
- const contentBeforeTag = content.substring(0, tagIndex);
142
-
143
- // Regex to match Liquid block opening tags (for, if, unless, case, capture, tablerow, raw, comment)
144
- // These tags OPEN a block that needs to be closed with an "end" tag
145
- const blockOpeningTags = ['for', 'if', 'unless', 'case', 'capture', 'tablerow', 'raw', 'comment'];
146
- const blockOpeningPattern = blockOpeningTags.map(tag => `\\b${tag}\\b`).join('|');
147
- const blockOpeningRegex = new RegExp(`{%-?\\s*(${blockOpeningPattern})[^%]*%}`, 'gi');
148
-
149
- // Regex to match Liquid block closing tags (endfor, endif, endunless, endcase, endcapture, etc.)
150
- const blockClosingTags = ['endfor', 'endif', 'endunless', 'endcase', 'endcapture', 'endtablerow', 'endraw', 'endcomment'];
151
- const blockClosingPattern = blockClosingTags.map(tag => `\\b${tag}\\b`).join('|');
152
- const blockClosingRegex = new RegExp(`{%-?\\s*(${blockClosingPattern})[^%]*%}`, 'gi');
153
-
154
- // Find all block opening tags before tagIndex
155
- const blockOpenings = Array.from(contentBeforeTag.matchAll(blockOpeningRegex), (match) => match.index);
156
-
157
- // Find all block closing tags before tagIndex
158
- const blockClosings = Array.from(contentBeforeTag.matchAll(blockClosingRegex), (match) => match.index);
159
-
160
- // Count unmatched opening blocks (for, if, etc.)
161
- const openBlockCount = blockOpenings.length - blockClosings.length;
162
-
163
- // Also check for self-contained tags (assign, etc.) that create blocks from {% to %}
164
- // These are any {% %} tags that are not block-opening or block-closing tags
165
- const allBlockStarts = /{%-?/g;
166
- const allBlockEnds = /-?%}/g;
167
- const allStarts = Array.from(contentBeforeTag.matchAll(allBlockStarts), (match) => match.index);
168
- const allEnds = Array.from(contentBeforeTag.matchAll(allBlockEnds), (match) => match.index);
169
-
170
- // Check if we're inside a self-contained tag (more {% than %} before tagIndex, excluding block tags)
171
- const selfContainedCount = allStarts.length - allEnds.length;
172
-
173
- // We're inside a block if:
174
- // 1. We're inside an unclosed block-opening tag (for, if, etc.), OR
175
- // 2. We're inside a self-contained tag (assign, etc.)
176
- return openBlockCount > 0 || selfContainedCount > 0;
177
- };
16
+ /** Whitespace-tolerant check for {{ unsubscribe }}-style tag in content. */
17
+ const UNSUBSCRIBE_TAG_REGEX = /\{\{\s*unsubscribe\s*\}\}/;
18
+ export const hasUnsubscribeTag = (content) =>
19
+ typeof content === 'string' && UNSUBSCRIBE_TAG_REGEX.test(content);
178
20
 
179
21
  /**
180
22
  * Shared core for tag validation. Used by validateTags (tagValidations) and FormBuilder.validateTags.
@@ -187,7 +29,7 @@ export const isInsideLiquidBlock = (content, tagIndex) => {
187
29
  * @param {Array} [params.initialMissingTags=null] - If set, use instead of computing from definitions.
188
30
  * @param {function} [params.skipTagsFn=skipTags] - skipTags implementation.
189
31
  * @param {boolean} [params.includeIsContentEmpty=false] - If true, response includes isContentEmpty: false.
190
- * @returns {{ valid: boolean, missingTags: string[], unsupportedTags: string[], isBraceError: boolean }}
32
+ * @returns {{ valid: boolean, missingTags: string[], isBraceError: boolean }}
191
33
  */
192
34
  export const validateTagsCore = ({
193
35
  contentForBraceCheck,
@@ -202,7 +44,6 @@ export const validateTagsCore = ({
202
44
  const response = {
203
45
  valid: true,
204
46
  missingTags: [],
205
- unsupportedTags: [],
206
47
  isBraceError: false,
207
48
  ...(includeIsContentEmpty && { isContentEmpty: false }),
208
49
  };
@@ -219,7 +60,7 @@ export const validateTagsCore = ({
219
60
  if (value === 'unsubscribe') {
220
61
  lodashForEach(supportedModules, (module) => {
221
62
  if (module.mandatory && (currentModule === module.context)) {
222
- if (contentForUnsubscribeScan.indexOf(`{{${value}}}`) === -1) {
63
+ if (!hasUnsubscribeTag(contentForUnsubscribeScan)) {
223
64
  response.missingTags.push(value);
224
65
  }
225
66
  }
@@ -266,7 +107,7 @@ export const validateTagsCore = ({
266
107
  * @param {Object} [params.location] - Location with query.module.
267
108
  * @param {string} [params.tagModule] - Override for current module context.
268
109
  * @param {boolean} [params.isFullMode] - If true, skip unsubscribe checks.
269
- * @returns {{ valid: boolean, missingTags: string[], unsupportedTags: string[], isBraceError: boolean }}
110
+ * @returns {{ valid: boolean, missingTags: string[], isBraceError: boolean }}
270
111
  */
271
112
  export const validateTags = ({
272
113
  content,