@capillarytech/creatives-library 8.0.345-alpha.13 → 8.0.345-alpha.15

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 (138) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/api.js +0 -20
  4. package/services/tests/api.test.js +13 -59
  5. package/utils/commonUtils.js +19 -1
  6. package/utils/rcsPayloadUtils.js +92 -0
  7. package/utils/templateVarUtils.js +201 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +167 -109
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapCustomSkeleton/index.js +1 -1
  15. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  16. package/v2Components/CapTagList/index.js +10 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +676 -186
  32. package/v2Components/CommonTestAndPreview/messages.js +49 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  46. package/v2Components/FormBuilder/index.js +8 -10
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +955 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -28
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  71. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  72. package/v2Containers/CreativesContainer/SlideBoxFooter.js +11 -4
  73. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  74. package/v2Containers/CreativesContainer/constants.js +9 -0
  75. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  76. package/v2Containers/CreativesContainer/index.js +300 -108
  77. package/v2Containers/CreativesContainer/index.scss +51 -1
  78. package/v2Containers/CreativesContainer/messages.js +0 -4
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -18
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  88. package/v2Containers/Rcs/constants.js +119 -8
  89. package/v2Containers/Rcs/index.js +2379 -807
  90. package/v2Containers/Rcs/index.js.rej +1336 -0
  91. package/v2Containers/Rcs/index.scss +276 -6
  92. package/v2Containers/Rcs/index.scss.rej +74 -0
  93. package/v2Containers/Rcs/messages.js +38 -3
  94. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  95. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  96. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  97. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  98. package/v2Containers/Rcs/tests/index.test.js +152 -121
  99. package/v2Containers/Rcs/tests/mockData.js +38 -0
  100. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  101. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  102. package/v2Containers/Rcs/utils.js +478 -11
  103. package/v2Containers/Sms/Create/index.js +100 -40
  104. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  105. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  106. package/v2Containers/SmsTrai/Create/index.js +9 -4
  107. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  108. package/v2Containers/SmsTrai/Edit/index.js +636 -130
  109. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  110. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  111. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  112. package/v2Containers/SmsWrapper/index.js +37 -8
  113. package/v2Containers/TagList/index.js +6 -0
  114. package/v2Containers/Templates/ChannelTypeIllustration.js +6 -23
  115. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  116. package/v2Containers/Templates/_templates.scss +181 -126
  117. package/v2Containers/Templates/actions.js +11 -36
  118. package/v2Containers/Templates/constants.js +2 -23
  119. package/v2Containers/Templates/index.js +142 -333
  120. package/v2Containers/Templates/messages.js +0 -68
  121. package/v2Containers/Templates/reducer.js +0 -68
  122. package/v2Containers/Templates/sagas.js +55 -98
  123. package/v2Containers/Templates/selectors.js +0 -12
  124. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +0 -12
  125. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  126. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1042 -1256
  127. package/v2Containers/Templates/tests/index.test.js +0 -6
  128. package/v2Containers/Templates/tests/reducer.test.js +0 -178
  129. package/v2Containers/Templates/tests/sagas.test.js +200 -436
  130. package/v2Containers/Templates/tests/selector.test.js +0 -32
  131. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  132. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  133. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  134. package/v2Containers/TemplatesV2/index.js +86 -23
  135. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  136. package/v2Containers/Whatsapp/index.js +3 -20
  137. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  138. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +0 -9
@@ -159,6 +159,24 @@ export const TAG_CONTENT_REGEX = /{{([^}]+)}}/g;
159
159
  export const ENTRY_TRIGGER_TAG_REGEX = /\bentryTrigger\.\w+(?:\.\w+)?(?:\(\w+\))?/g;
160
160
  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"];
161
161
 
162
+ // --- Template variable tokens (`{{var}}`, DLT `{#var#}`) ---
163
+ /** Global: all `{{…}}` placeholders in a string. */
164
+ export const DEFAULT_MUSTACHE_VAR_REGEX = /\{\{[^}]+\}\}/g;
165
+ /** Global: `{{…}}` or DLT `{#…#}` tokens (SMS combined mode). */
166
+ export const COMBINED_SMS_TEMPLATE_VAR_REGEX = /\{\{[^}]+\}\}|\{\#[^#]*\#\}/g;
167
+ /** Full-string check: one mustache token. */
168
+ export const MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX = /^\{\{[^}]+\}\}$/;
169
+ /** Full-string check: one DLT hash token. */
170
+ export const DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX = /^\{\#[^#]*\#\}$/;
171
+ /** Full-string with capture group: inner name for `{{ name }}`-style tokens (whitespace-tolerant). */
172
+ export const MUSTACHE_TOKEN_INNER_NAME_REGEX = /^\{\{\s*([^}]+?)\s*\}\}$/;
173
+ /** Full-string with capture group: inner name/body for `{# name #}` DLT tokens (whitespace-tolerant). */
174
+ export const DLT_HASH_TOKEN_INNER_NAME_REGEX = /^\{#\s*(.*?)\s*#\}$/;
175
+ /** Global with capture group: inner name inside `{{name}}`. */
176
+ export const MUSTACHE_VAR_NAME_CAPTURE_REGEX = /\{\{([^}]+)\}\}/g;
177
+ /** Global with capture group: inner body inside `{#body#}`. */
178
+ export const DLT_VAR_BODY_CAPTURE_REGEX = /\{\#([^#]*)\#\}/g;
179
+
162
180
  export const GET_TRANSLATION_MAPPED = {
163
181
  'en': 'en-US',
164
182
  'zh-cn': 'zh',
@@ -196,3 +214,14 @@ export const LOGOUT_FAILURE = 'cap/LOGOUT_FAILURE';
196
214
  export const JAPANESE_HELP_TEXT = 'ヘルプ :トークンの定義';
197
215
 
198
216
  export const TAG_TRANSLATION_DOC = 'https://docs.capillarytech.com/docs/tags-translation';
217
+
218
+ // --- RCS SMS fallback API contract (shared across modules: campaigns, journey, etc.) ---
219
+
220
+ /** Keys on `messageContent.*.smsFallBackContent` sent to the API: only `message` + `templateConfigs`.
221
+ * `rcsSmsFallbackVarMapped` is editor-only — merged into `message` at normalize, not sent on the wire. */
222
+ export const RCS_API_SMS_FALLBACK_KEYS = Object.freeze({
223
+ MESSAGE: 'message',
224
+ TEMPLATE_CONFIGS: 'templateConfigs',
225
+ RCS_SMS_FALLBACK_VAR_MAPPED: 'rcsSmsFallbackVarMapped',
226
+ });
227
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.345-alpha.13",
4
+ "version": "8.0.345-alpha.15",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -361,26 +361,6 @@ export const deleteTemplate = ({channel, id}) => {
361
361
  //return API.deleteResource(url);
362
362
  };
363
363
 
364
- export const archiveTemplate = ({ channel, id }) => {
365
- const url = `${API_ENDPOINT}/templates/archive/${id}/${channel}`;
366
- return request(url, getAPICallObject('PUT'));
367
- };
368
-
369
- export const unarchiveTemplate = ({ channel, id }) => {
370
- const url = `${API_ENDPOINT}/templates/unarchive/${id}/${channel}`;
371
- return request(url, getAPICallObject('PUT'));
372
- };
373
-
374
- export const bulkArchiveTemplates = ({ channel, ids }) => {
375
- const url = `${API_ENDPOINT}/templates/archive/bulk/${channel}`;
376
- return request(url, getAPICallObject('PUT', { ids }));
377
- };
378
-
379
- export const bulkUnarchiveTemplates = ({ channel, ids }) => {
380
- const url = `${API_ENDPOINT}/templates/unarchive/bulk/${channel}`;
381
- return request(url, getAPICallObject('PUT', { ids }));
382
- };
383
-
384
364
  export const deleteRcsTemplate = ({ templateName }) => {
385
365
  const url = `${API_ENDPOINT}/templates/${templateName}/RCS`;
386
366
  return request(url, getAPICallObject('DELETE'))
@@ -1042,6 +1042,19 @@ describe('getMembersLookup', () => {
1042
1042
  getMembersLookup('email', 'user+test@example.com');
1043
1043
  expect(global.fetch).toHaveBeenCalled();
1044
1044
  const calls = global.fetch.mock.calls;
1045
+ const withEncoding = calls.find(
1046
+ (c) =>
1047
+ c[0] &&
1048
+ String(c[0]).includes('members') &&
1049
+ String(c[0]).includes('identifierType=email') &&
1050
+ String(c[0]).includes('user%2Btest%40example.com')
1051
+ );
1052
+ if (withEncoding) {
1053
+ const [url, options] = withEncoding;
1054
+ expect(url).toContain('identifierType=email');
1055
+ expect(url).toContain('identifierValue=user%2Btest%40example.com');
1056
+ expect(options?.method || 'GET').toBe('GET');
1057
+ }
1045
1058
  const anyMembersCall = calls.find((c) => c[0] && String(c[0]).includes('members'));
1046
1059
  expect(anyMembersCall).toBeDefined();
1047
1060
  expect(anyMembersCall[0]).toContain('identifierType=');
@@ -1086,62 +1099,3 @@ describe('createTestCustomer', () => {
1086
1099
  expect(lastCall[1].body).toBe(JSON.stringify(payload));
1087
1100
  });
1088
1101
  });
1089
-
1090
- import {
1091
- archiveTemplate,
1092
- unarchiveTemplate,
1093
- bulkArchiveTemplates,
1094
- bulkUnarchiveTemplates,
1095
- } from '../api';
1096
-
1097
- describe('archiveTemplate', () => {
1098
- beforeEach(() => {
1099
- global.fetch = jest.fn().mockReturnValue(Promise.resolve({ json: () => Promise.resolve({}) }));
1100
- });
1101
-
1102
- it('should call PUT on archive endpoint', () => {
1103
- archiveTemplate({ channel: 'EMAIL', id: 'id123' });
1104
- expect(global.fetch).toHaveBeenCalled();
1105
- const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
1106
- expect(lastCall[1].method).toBe('PUT');
1107
- });
1108
- });
1109
-
1110
- describe('unarchiveTemplate', () => {
1111
- beforeEach(() => {
1112
- global.fetch = jest.fn().mockReturnValue(Promise.resolve({ json: () => Promise.resolve({}) }));
1113
- });
1114
-
1115
- it('should call PUT on unarchive endpoint', () => {
1116
- unarchiveTemplate({ channel: 'EMAIL', id: 'id123' });
1117
- expect(global.fetch).toHaveBeenCalled();
1118
- const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
1119
- expect(lastCall[1].method).toBe('PUT');
1120
- });
1121
- });
1122
-
1123
- describe('bulkArchiveTemplates', () => {
1124
- beforeEach(() => {
1125
- global.fetch = jest.fn().mockReturnValue(Promise.resolve({ json: () => Promise.resolve({}) }));
1126
- });
1127
-
1128
- it('should call PUT on bulk archive endpoint with ids', () => {
1129
- bulkArchiveTemplates({ channel: 'EMAIL', ids: ['id1', 'id2'] });
1130
- expect(global.fetch).toHaveBeenCalled();
1131
- const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
1132
- expect(lastCall[1].method).toBe('PUT');
1133
- });
1134
- });
1135
-
1136
- describe('bulkUnarchiveTemplates', () => {
1137
- beforeEach(() => {
1138
- global.fetch = jest.fn().mockReturnValue(Promise.resolve({ json: () => Promise.resolve({}) }));
1139
- });
1140
-
1141
- it('should call PUT on bulk unarchive endpoint with ids', () => {
1142
- bulkUnarchiveTemplates({ channel: 'EMAIL', ids: ['id1', 'id2'] });
1143
- expect(global.fetch).toHaveBeenCalled();
1144
- const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
1145
- expect(lastCall[1].method).toBe('PUT');
1146
- });
1147
- });
@@ -539,4 +539,22 @@ export const isValidMobile = (mobile) => PHONE_REGEX.test(mobile);
539
539
  export const formatPhoneNumber = (phone) => {
540
540
  if (!phone) return '';
541
541
  return String(phone).replace(/[^\d]/g, '');
542
- };
542
+ };
543
+
544
+ /**
545
+ * TRAI sender IDs on persisted RCS SMS fallback: may live on the merged object, under
546
+ * `templateConfigs`, or (legacy) `templateConfigs.header`. Same resolution order as
547
+ * `createPayload` in `Rcs/index.js`.
548
+ */
549
+ export function extractRegisteredSenderIdsFromSmsFallbackRecord(record) {
550
+ if (!record || typeof record !== 'object') return null;
551
+ const tc = record.templateConfigs && typeof record.templateConfigs === 'object'
552
+ ? record.templateConfigs
553
+ : {};
554
+ const candidates = [record.registeredSenderIds, tc.registeredSenderIds, tc.header];
555
+ for (let i = 0; i < candidates.length; i += 1) {
556
+ const a = candidates[i];
557
+ if (Array.isArray(a) && a.length > 0) return a;
558
+ }
559
+ return null;
560
+ }
@@ -0,0 +1,92 @@
1
+ import isEmpty from 'lodash/isEmpty';
2
+ import { RCS_API_SMS_FALLBACK_KEYS } from '../constants/unified';
3
+ import {
4
+ COMBINED_SMS_TEMPLATE_VAR_REGEX,
5
+ isAnyTemplateVarToken,
6
+ splitTemplateVarString,
7
+ } from './templateVarUtils';
8
+
9
+ const {
10
+ MESSAGE,
11
+ TEMPLATE_CONFIGS,
12
+ RCS_SMS_FALLBACK_VAR_MAPPED,
13
+ } = RCS_API_SMS_FALLBACK_KEYS;
14
+
15
+ function mergeSmsFallbackSlots(templateStr, varMapped) {
16
+ const map = varMapped != null && typeof varMapped === 'object' ? varMapped : {};
17
+ if (!Object.keys(map).length) return typeof templateStr === 'string' ? templateStr : '';
18
+ const str = typeof templateStr === 'string' ? templateStr : '';
19
+ return splitTemplateVarString(str, COMBINED_SMS_TEMPLATE_VAR_REGEX)
20
+ .map((seg, i) => {
21
+ if (!isAnyTemplateVarToken(seg)) return seg;
22
+ const key = `${seg}_${i}`;
23
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
24
+ const v = map[key];
25
+ return v == null ? '' : String(v);
26
+ }
27
+ return seg;
28
+ })
29
+ .join('');
30
+ }
31
+
32
+ /**
33
+ * Mutates one RCS messageContent entry: strips UI-only fields, promotes smsFallBackContent to a
34
+ * sibling of rcsContent, and folds rcsSmsFallbackVarMapped into the message string for the API.
35
+ */
36
+ export const normalizeRcsMessageContentForApi = messageContentItem => {
37
+ const {
38
+ rcsContent = {},
39
+ smsFallBackContent: rootSmsFallbackContent = {},
40
+ } = messageContentItem;
41
+ const {
42
+ smsFallBackContent: legacySmsFallbackNestedUnderCard,
43
+ ...rcsCardPayloadOnly
44
+ } = { ...rcsContent };
45
+ /* Legacy nested first, then root — so current root `smsFallBackContent` wins on key clashes. */
46
+ const mergedSmsFallbackSources = {
47
+ ...(legacySmsFallbackNestedUnderCard ?? {}),
48
+ ...rootSmsFallbackContent,
49
+ };
50
+ messageContentItem.rcsContent = rcsCardPayloadOnly;
51
+
52
+ delete messageContentItem.templateConfigs;
53
+ delete rcsCardPayloadOnly.templateConfigs;
54
+
55
+ if (Array.isArray(rcsCardPayloadOnly.cardContent)) {
56
+ rcsCardPayloadOnly.cardContent.forEach(card => {
57
+ if (Array.isArray(card.suggestions)) {
58
+ card.suggestions.forEach(suggestion => {
59
+ delete suggestion.isSaved;
60
+ });
61
+ }
62
+ });
63
+ }
64
+
65
+ const varMappedMerged = mergedSmsFallbackSources[RCS_SMS_FALLBACK_VAR_MAPPED];
66
+ const hasVarMapped =
67
+ varMappedMerged != null
68
+ && typeof varMappedMerged === 'object'
69
+ && Object.keys(varMappedMerged).length > 0;
70
+ const hasMessageKey = Object.prototype.hasOwnProperty.call(mergedSmsFallbackSources, MESSAGE);
71
+ const rawMessage = hasMessageKey ? mergedSmsFallbackSources[MESSAGE] : undefined;
72
+ const messageForApi =
73
+ hasMessageKey && hasVarMapped
74
+ ? mergeSmsFallbackSlots(rawMessage == null ? '' : String(rawMessage), varMappedMerged)
75
+ : rawMessage;
76
+
77
+ const apiSiblingSmsFallback = Object.fromEntries(
78
+ [
79
+ hasMessageKey && [MESSAGE, messageForApi],
80
+ !isEmpty(mergedSmsFallbackSources[TEMPLATE_CONFIGS]) && [
81
+ TEMPLATE_CONFIGS,
82
+ mergedSmsFallbackSources[TEMPLATE_CONFIGS],
83
+ ],
84
+ ].filter(Boolean),
85
+ );
86
+
87
+ if (Object.keys(apiSiblingSmsFallback).length) {
88
+ messageContentItem.smsFallBackContent = apiSiblingSmsFallback;
89
+ } else {
90
+ delete messageContentItem.smsFallBackContent;
91
+ }
92
+ };
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Shared utilities for templates containing {{var}} and DLT `{#var#}` tokens.
3
+ * Same split process used by WhatsApp/RCS: match vars with regex, then split content at each var.
4
+ */
5
+
6
+ import {
7
+ COMBINED_SMS_TEMPLATE_VAR_REGEX,
8
+ DEFAULT_MUSTACHE_VAR_REGEX,
9
+ DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX,
10
+ DLT_HASH_TOKEN_INNER_NAME_REGEX,
11
+ DLT_VAR_BODY_CAPTURE_REGEX,
12
+ MUSTACHE_TOKEN_INNER_NAME_REGEX,
13
+ MUSTACHE_VAR_NAME_CAPTURE_REGEX,
14
+ MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX,
15
+ } from '../constants/unified';
16
+
17
+ export { COMBINED_SMS_TEMPLATE_VAR_REGEX, DEFAULT_MUSTACHE_VAR_REGEX } from '../constants/unified';
18
+
19
+ const isMustacheVarToken = (s) =>
20
+ typeof s === 'string' && MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX.test(s);
21
+
22
+ export const isDltHashVarToken = (s) =>
23
+ typeof s === 'string' && DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX.test(s);
24
+
25
+ export const isAnyTemplateVarToken = (s) => isMustacheVarToken(s) || isDltHashVarToken(s);
26
+
27
+ /**
28
+ * `RegExp.prototype.exec` only advances `lastIndex` when the `g` and/or `y` flag is set.
29
+ * A non-global regex in a `while ((m = re.exec(s)) !== null)` loop never advances and can run forever.
30
+ *
31
+ * @param {RegExp} regex
32
+ * @returns {RegExp} Same instance if already global; otherwise a new RegExp with `g` appended to flags.
33
+ */
34
+ function ensureGlobalRegexForExecLoop(regex) {
35
+ if (!regex || !(regex instanceof RegExp) || regex.global) {
36
+ return regex;
37
+ }
38
+ return new RegExp(regex.source, `${regex.flags}g`);
39
+ }
40
+
41
+ /**
42
+ * Splits `content` into alternating plain-text segments and variable tokens, using the order of
43
+ * matches in `matchedVariableTokens` (e.g. from `String.prototype.match` with a global regex).
44
+ *
45
+ * @param {string[]} matchedVariableTokens - Matched tokens in left-to-right order
46
+ * @param {string} content - Full template string
47
+ * @returns {string[]}
48
+ */
49
+ export const splitContentByOrderedVarTokens = (matchedVariableTokens, content) => {
50
+ const segmentList = [];
51
+ const tokenQueue = [...(matchedVariableTokens ?? [])];
52
+ let remainder = content ?? '';
53
+ while ((remainder?.length ?? 0) > 0) {
54
+ const nextVarToken = tokenQueue?.[0];
55
+ if (nextVarToken == null || nextVarToken === '') {
56
+ segmentList.push(remainder);
57
+ break;
58
+ }
59
+ const varStartIndex = remainder.indexOf(nextVarToken);
60
+ if (varStartIndex !== -1) {
61
+ segmentList.push(remainder.substring(0, varStartIndex));
62
+ segmentList.push(nextVarToken);
63
+ const afterVar = varStartIndex + (nextVarToken?.length ?? 0);
64
+ remainder = remainder.substring(afterVar, remainder?.length ?? 0);
65
+ tokenQueue.shift();
66
+ } else {
67
+ segmentList.push(remainder);
68
+ break;
69
+ }
70
+ }
71
+ return segmentList;
72
+ };
73
+
74
+ /**
75
+ * Splits a template string into an array of text + {{var}} segments using the given regex.
76
+ *
77
+ * @param {string} str - Template string
78
+ * @param {RegExp} [varRegex] - Regex that matches var tokens; defaults to DEFAULT_MUSTACHE_VAR_REGEX
79
+ * @returns {string[]}
80
+ */
81
+ export const splitTemplateVarString = (str = '', varRegex = DEFAULT_MUSTACHE_VAR_REGEX) => {
82
+ if (!str) return [];
83
+ const matchedVariableTokens = str.match(varRegex) || [];
84
+ return splitContentByOrderedVarTokens(matchedVariableTokens, str).filter(
85
+ (segment) => segment === 0 || segment
86
+ );
87
+ };
88
+
89
+ /**
90
+ * Extracts unique variable names from `{{var}}` (and, when using default capture, `{#var#}`) strings.
91
+ *
92
+ * @param {string} templateStr
93
+ * @param {RegExp} [captureRegex] - regex with a single capture group for the var name; if omitted, also scans DLT `{#…#}`.
94
+ * @returns {string[]}
95
+ */
96
+ export const extractTemplateVariables = (templateStr = '', captureRegex) => {
97
+ if (!templateStr) return [];
98
+ const variables = [];
99
+ const add = (name) => {
100
+ const n = (name || '').trim();
101
+ if (n && !variables.includes(n)) variables.push(n);
102
+ };
103
+ const mustacheRe = ensureGlobalRegexForExecLoop(
104
+ captureRegex || MUSTACHE_VAR_NAME_CAPTURE_REGEX,
105
+ );
106
+ let match;
107
+ while ((match = mustacheRe.exec(templateStr)) !== null) {
108
+ add(match?.[1]);
109
+ }
110
+ if (!captureRegex) {
111
+ const dltRe = new RegExp(DLT_VAR_BODY_CAPTURE_REGEX.source, DLT_VAR_BODY_CAPTURE_REGEX.flags);
112
+ let dltMatch;
113
+ while ((dltMatch = dltRe.exec(templateStr)) !== null) {
114
+ add(dltMatch?.[1] || 'var');
115
+ }
116
+ }
117
+ return variables;
118
+ };
119
+
120
+ /**
121
+ * Looks up the inner name of a `{{name}}` or `{#name#}` token in a flat key→value map.
122
+ * Handles both exact matches and dot-path suffixes (e.g. `tag.FORMAT_1` → name `FORMAT_1`).
123
+ * Returns the resolved string value, or `undefined` if not found / blank.
124
+ */
125
+ function resolveUserVarForToken(segment, userVarMap) {
126
+ if (!userVarMap || typeof userVarMap !== 'object') return undefined;
127
+ const mMustache = segment.match(MUSTACHE_TOKEN_INNER_NAME_REGEX);
128
+ const mDlt = segment.match(DLT_HASH_TOKEN_INNER_NAME_REGEX);
129
+ const innerName = ((mMustache ? mMustache[1] : (mDlt ? mDlt[1] : '')) ?? '').trim();
130
+ if (!innerName) return undefined;
131
+ const direct = userVarMap[innerName];
132
+ if (direct != null && String(direct).trim() !== '') return String(direct);
133
+ const hit = Object.keys(userVarMap).find((k) => k === innerName || k.endsWith(`.${innerName}`));
134
+ if (hit != null && userVarMap[hit] != null && String(userVarMap[hit]).trim() !== '') return String(userVarMap[hit]);
135
+ return undefined;
136
+ }
137
+
138
+ /**
139
+ * SMS / DLT template preview: replace `{{…}}` / `{#…#}` tokens using `varMapData` keys `${token}_${index}`.
140
+ * Used by SmsTraiEdit (RCS SMS fallback) and UnifiedPreview fallback SMS bubble.
141
+ * DLT `{#…#}`: empty / unset slot values show the raw token (matches DLT preview UX); mustache `{{…}}` still
142
+ * resolves to empty when the slot is cleared.
143
+ *
144
+ * @param {string} templateStr
145
+ * @param {Object} varMapData - Slot map (`${token}_${index}` → value)
146
+ * @param {Object|null} [userVarMap] - Optional Test & Preview custom values; used as fallback when a slot
147
+ * is absent or blank. When omitted the function behaves identically to its original two-argument form.
148
+ */
149
+ export const getFallbackResolvedContent = (templateStr = '', varMapData = {}, userVarMap = null) => {
150
+ const fallbackVarSlotMap = varMapData ?? {};
151
+ const templateSegments = splitTemplateVarString(templateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX);
152
+ return templateSegments
153
+ .map((segment, segmentIndex) => {
154
+ const isVariableToken = typeof segment === 'string' && isAnyTemplateVarToken(segment);
155
+ if (!isVariableToken) return segment;
156
+ const slotKey = `${segment}_${segmentIndex}`;
157
+ if (Object.prototype.hasOwnProperty.call(fallbackVarSlotMap, slotKey)) {
158
+ const slotValue = fallbackVarSlotMap[slotKey];
159
+ if (isDltHashVarToken(segment)) {
160
+ if (slotValue == null || String(slotValue).trim() === '') {
161
+ if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
162
+ return segment;
163
+ }
164
+ return String(slotValue);
165
+ }
166
+ if (slotValue != null && String(slotValue).trim() !== '') return String(slotValue);
167
+ if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
168
+ return '';
169
+ }
170
+ if (userVarMap) { const uv = resolveUserVarForToken(segment, userVarMap); if (uv !== undefined) return uv; }
171
+ if (isDltHashVarToken(segment)) return segment;
172
+ return '';
173
+ })
174
+ .join('');
175
+ };
176
+
177
+ /**
178
+ * SMS fallback **card** (library list): show filled slot text; if a slot is empty, unset, or
179
+ * whitespace-only, show the raw `{{…}}` / `{#…#}` token so “save without labels” still shows
180
+ * `{#var#}` in the card. {@link getFallbackResolvedContent} keeps DLT placeholders in preview when
181
+ * slots are empty; mustache cleared slots can still render empty.
182
+ */
183
+ export const getFallbackResolvedContentForCardDisplay = (templateStr = '', varMapData = {}) => {
184
+ const fallbackVarSlotMap = varMapData ?? {};
185
+ const templateSegments = splitTemplateVarString(templateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX);
186
+ return templateSegments
187
+ .map((segment, segmentIndex) => {
188
+ const isVariableToken = typeof segment === 'string' && isAnyTemplateVarToken(segment);
189
+ if (!isVariableToken) return segment;
190
+ const slotKey = `${segment}_${segmentIndex}`;
191
+ if (Object.prototype.hasOwnProperty.call(fallbackVarSlotMap, slotKey)) {
192
+ const slotValue = fallbackVarSlotMap[slotKey];
193
+ if (slotValue == null) return segment;
194
+ const str = String(slotValue);
195
+ if (str.trim() === '') return segment;
196
+ return str;
197
+ }
198
+ return segment;
199
+ })
200
+ .join('');
201
+ };
@@ -0,0 +1,204 @@
1
+ import {
2
+ extractTemplateVariables,
3
+ splitTemplateVarString,
4
+ splitContentByOrderedVarTokens,
5
+ getFallbackResolvedContent,
6
+ getFallbackResolvedContentForCardDisplay,
7
+ isDltHashVarToken,
8
+ isAnyTemplateVarToken,
9
+ } from '../templateVarUtils';
10
+
11
+ describe('templateVarUtils', () => {
12
+ describe('splitContentByOrderedVarTokens', () => {
13
+ it('pushes remainder when next token is not found in string', () => {
14
+ expect(splitContentByOrderedVarTokens(['{{b}}'], 'hello')).toEqual(['hello']);
15
+ });
16
+
17
+ it('handles empty token queue by pushing remainder', () => {
18
+ expect(splitContentByOrderedVarTokens([], 'rest')).toEqual(['rest']);
19
+ });
20
+
21
+ it('handles null matchedVariableTokens as empty queue', () => {
22
+ expect(splitContentByOrderedVarTokens(null, 'abc')).toEqual(['abc']);
23
+ });
24
+
25
+ it('stops and pushes remainder when next token in queue is empty string', () => {
26
+ expect(splitContentByOrderedVarTokens(['{{a}}', ''], 'x{{a}}y')).toEqual(['x', '{{a}}', 'y']);
27
+ });
28
+
29
+ it('splits multiple ordered tokens across content', () => {
30
+ expect(splitContentByOrderedVarTokens(['{{a}}', '{{b}}'], 'p{{a}}q{{b}}r')).toEqual([
31
+ 'p',
32
+ '{{a}}',
33
+ 'q',
34
+ '{{b}}',
35
+ 'r',
36
+ ]);
37
+ });
38
+
39
+ it('returns empty segment list when content is null and queue is empty', () => {
40
+ expect(splitContentByOrderedVarTokens([], null)).toEqual([]);
41
+ });
42
+ });
43
+
44
+ describe('splitTemplateVarString', () => {
45
+ it('returns empty array for falsy string', () => {
46
+ expect(splitTemplateVarString('')).toEqual([]);
47
+ expect(splitTemplateVarString(null)).toEqual([]);
48
+ });
49
+
50
+ it('splits mustache segments', () => {
51
+ const parts = splitTemplateVarString('Hi {{name}}!');
52
+ expect(parts.some((p) => p.includes('name'))).toBe(true);
53
+ });
54
+
55
+ it('omits empty segments after filtering', () => {
56
+ expect(splitTemplateVarString('a{{x}}b')).toEqual(['a', '{{x}}', 'b']);
57
+ });
58
+ });
59
+
60
+ describe('getFallbackResolvedContent', () => {
61
+ it('replaces var segments from varMapData', () => {
62
+ const out = getFallbackResolvedContent('{{a}}', { '{{a}}_0': 'X' });
63
+ expect(out).toBe('X');
64
+ });
65
+
66
+ it('keeps DLT token string when slot empty', () => {
67
+ const out = getFallbackResolvedContent('{#v#}', {});
68
+ expect(out).toContain('#');
69
+ });
70
+
71
+ it('returns empty string for mustache slot when map has no value', () => {
72
+ const out = getFallbackResolvedContent('Hi {{missing}}', {});
73
+ expect(out).toBe('Hi ');
74
+ });
75
+
76
+ it('treats null as empty; explicit empty string is kept (cleared input)', () => {
77
+ expect(getFallbackResolvedContent('{{a}}', { '{{a}}_0': null })).toBe('');
78
+ expect(getFallbackResolvedContent('{{a}}', { '{{a}}_0': '' })).toBe('');
79
+ });
80
+
81
+ it('keeps DLT token in preview when slot map has empty string (no tag yet)', () => {
82
+ expect(getFallbackResolvedContent('{#v#}', { '{#v#}_0': '' })).toBe('{#v#}');
83
+ });
84
+
85
+ it('keeps DLT token in preview when slot map has null', () => {
86
+ expect(getFallbackResolvedContent('{#v#}', { '{#v#}_0': null })).toBe('{#v#}');
87
+ });
88
+
89
+ it('uses slot replacement when value is non-empty string', () => {
90
+ expect(getFallbackResolvedContent('A{{b}}C', { '{{b}}_1': 'ok' })).toBe('AokC');
91
+ });
92
+ });
93
+
94
+ describe('getFallbackResolvedContentForCardDisplay', () => {
95
+ it('shows raw tokens when map is empty or slots unset', () => {
96
+ expect(getFallbackResolvedContentForCardDisplay('Hi {{name}}', {})).toBe('Hi {{name}}');
97
+ expect(getFallbackResolvedContentForCardDisplay('{#var#}', {})).toBe('{#var#}');
98
+ });
99
+
100
+ it('shows raw token when slot key exists but value is empty (no labels)', () => {
101
+ expect(getFallbackResolvedContentForCardDisplay('{#var#}', { '{#var#}_0': '' })).toBe('{#var#}');
102
+ expect(getFallbackResolvedContentForCardDisplay('{{a}}', { '{{a}}_0': '' })).toBe('{{a}}');
103
+ });
104
+
105
+ it('replaces when slot has non-empty value', () => {
106
+ expect(getFallbackResolvedContentForCardDisplay('Hi {{name}}', { '{{name}}_1': 'Pat' })).toBe('Hi Pat');
107
+ });
108
+ });
109
+
110
+ describe('isDltHashVarToken / isAnyTemplateVarToken', () => {
111
+ it('detects DLT hash token', () => {
112
+ expect(isDltHashVarToken('{#x#}')).toBe(true);
113
+ expect(isDltHashVarToken('plain')).toBe(false);
114
+ });
115
+
116
+ it('isAnyTemplateVarToken combines mustache and DLT', () => {
117
+ expect(isAnyTemplateVarToken('{{a}}')).toBe(true);
118
+ expect(isAnyTemplateVarToken('{#x#}')).toBe(true);
119
+ });
120
+ });
121
+
122
+ describe('extractTemplateVariables', () => {
123
+ it('returns empty array for empty or missing template', () => {
124
+ expect(extractTemplateVariables('')).toEqual([]);
125
+ expect(extractTemplateVariables()).toEqual([]);
126
+ });
127
+
128
+ it('includes DLT {#var#} names when no custom capture regex', () => {
129
+ const vars = extractTemplateVariables('Hello {#promo#} and {{name}}');
130
+ expect(vars).toEqual(expect.arrayContaining(['promo', 'name']));
131
+ });
132
+
133
+ it('skips DLT scan when captureRegex is provided', () => {
134
+ const re = /\{\{([^}]+)\}\}/g;
135
+ expect(extractTemplateVariables('{#x#} {{y}}', re)).toEqual(['y']);
136
+ });
137
+
138
+ it('dedupes duplicate variable names', () => {
139
+ expect(extractTemplateVariables('{{a}} {{a}}')).toEqual(['a']);
140
+ });
141
+
142
+ it('uses default name var when DLT body capture is empty', () => {
143
+ const vars = extractTemplateVariables('{##}');
144
+ expect(vars).toContain('var');
145
+ });
146
+ });
147
+
148
+ describe('extractTemplateVariables (regex global)', () => {
149
+ it('extracts all vars when caller passes a non-global capture regex (avoids infinite exec loop)', () => {
150
+ const nonGlobal = /\{\{([^}]+)\}\}/;
151
+ expect(nonGlobal.global).toBe(false);
152
+ expect(extractTemplateVariables('{{a}} and {{b}}', nonGlobal)).toEqual(['a', 'b']);
153
+ });
154
+
155
+ it('still works when capture regex already has the global flag', () => {
156
+ const globalRe = /\{\{([^}]+)\}\}/g;
157
+ expect(extractTemplateVariables('{{x}} {{y}}', globalRe)).toEqual(['x', 'y']);
158
+ });
159
+ });
160
+
161
+ describe('getFallbackResolvedContent — userVarMap (third param)', () => {
162
+ it('falls back to userVarMap for mustache slot absent from varMapData', () => {
163
+ expect(getFallbackResolvedContent('Hi {{name}}', {}, { name: 'Alice' })).toBe('Hi Alice');
164
+ });
165
+
166
+ it('falls back to userVarMap for DLT slot absent from varMapData', () => {
167
+ expect(getFallbackResolvedContent('{#city#}', {}, { city: 'Mumbai' })).toBe('Mumbai');
168
+ });
169
+
170
+ it('falls back to userVarMap when mustache slot is present but blank', () => {
171
+ expect(getFallbackResolvedContent('{{x}}', { '{{x}}_0': '' }, { x: 'val' })).toBe('val');
172
+ });
173
+
174
+ it('falls back to userVarMap when DLT slot is present but blank', () => {
175
+ expect(getFallbackResolvedContent('{#x#}', { '{#x#}_0': '' }, { x: 'val' })).toBe('val');
176
+ });
177
+
178
+ it('slot wins over userVarMap when mustache slot is non-empty', () => {
179
+ expect(getFallbackResolvedContent('{{x}}', { '{{x}}_0': 'slot-val' }, { x: 'user-val' })).toBe('slot-val');
180
+ });
181
+
182
+ it('slot wins over userVarMap when DLT slot is non-empty', () => {
183
+ expect(getFallbackResolvedContent('{#x#}', { '{#x#}_0': 'slot' }, { x: 'user' })).toBe('slot');
184
+ });
185
+
186
+ it('resolves dot-path suffix key from userVarMap', () => {
187
+ expect(
188
+ getFallbackResolvedContent('{{FORMAT_1}}', {}, { 'expiry.FORMAT_1': 'Dec 2025' }),
189
+ ).toBe('Dec 2025');
190
+ });
191
+
192
+ it('userVarMap null behaves identically to the two-argument form', () => {
193
+ expect(getFallbackResolvedContent('{{a}}', { '{{a}}_0': 'x' }, null)).toBe('x');
194
+ });
195
+
196
+ it('blank userVarMap value leaves mustache token as empty string', () => {
197
+ expect(getFallbackResolvedContent('{{a}}', {}, { a: '' })).toBe('');
198
+ });
199
+
200
+ it('blank userVarMap value leaves DLT token as raw token', () => {
201
+ expect(getFallbackResolvedContent('{#a#}', {}, { a: '' })).toBe('{#a#}');
202
+ });
203
+ });
204
+ });