@capillarytech/creatives-library 9.0.29 → 9.0.30

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 (147) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +261 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  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 +108 -15
  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 +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +322 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  96. package/v2Containers/Rcs/carouselUtils.js +224 -0
  97. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  98. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  99. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  100. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  101. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  102. package/v2Containers/Rcs/constants.js +132 -16
  103. package/v2Containers/Rcs/index.js +1822 -946
  104. package/v2Containers/Rcs/index.scss +443 -8
  105. package/v2Containers/Rcs/messages.js +45 -22
  106. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +494 -0
  107. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  108. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  109. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  110. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  111. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  112. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74440 -39387
  113. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  114. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  115. package/v2Containers/Rcs/tests/index.test.js +275 -40
  116. package/v2Containers/Rcs/tests/mockData.js +38 -0
  117. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +912 -0
  118. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  119. package/v2Containers/Rcs/utils.js +514 -12
  120. package/v2Containers/Sms/Create/index.js +115 -48
  121. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  122. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  123. package/v2Containers/SmsTrai/Create/index.js +9 -4
  124. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  125. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  126. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  127. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  128. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  129. package/v2Containers/SmsWrapper/index.js +37 -8
  130. package/v2Containers/TagList/index.js +5 -0
  131. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  132. package/v2Containers/Templates/_templates.scss +196 -12
  133. package/v2Containers/Templates/actions.js +11 -0
  134. package/v2Containers/Templates/constants.js +2 -0
  135. package/v2Containers/Templates/index.js +131 -59
  136. package/v2Containers/Templates/sagas.js +57 -13
  137. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  138. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  139. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  140. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  141. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  142. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  143. package/v2Containers/TemplatesV2/index.js +88 -25
  144. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  145. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  146. package/v2Containers/Whatsapp/index.js +3 -20
  147. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -162,6 +162,24 @@ export const TAG_CONTENT_REGEX = /{{([^}]+)}}/g;
162
162
  export const ENTRY_TRIGGER_TAG_REGEX = /\bentryTrigger\.\w+(?:\.\w+)?(?:\(\w+\))?/g;
163
163
  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"];
164
164
 
165
+ // --- Template variable tokens (`{{var}}`, DLT `{#var#}`) ---
166
+ /** Global: all `{{…}}` placeholders in a string. */
167
+ export const DEFAULT_MUSTACHE_VAR_REGEX = /\{\{[^}]+\}\}/g;
168
+ /** Global: `{{…}}` or DLT `{#…#}` tokens (SMS combined mode). */
169
+ export const COMBINED_SMS_TEMPLATE_VAR_REGEX = /\{\{[^}]+\}\}|\{\#[^#]*\#\}/g;
170
+ /** Full-string check: one mustache token. */
171
+ export const MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX = /^\{\{[^}]+\}\}$/;
172
+ /** Full-string check: one DLT hash token. */
173
+ export const DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX = /^\{\#[^#]*\#\}$/;
174
+ /** Full-string with capture group: inner name for `{{ name }}`-style tokens (whitespace-tolerant). */
175
+ export const MUSTACHE_TOKEN_INNER_NAME_REGEX = /^\{\{\s*([^}]+?)\s*\}\}$/;
176
+ /** Full-string with capture group: inner name/body for `{# name #}` DLT tokens (whitespace-tolerant). */
177
+ export const DLT_HASH_TOKEN_INNER_NAME_REGEX = /^\{#\s*(.*?)\s*#\}$/;
178
+ /** Global with capture group: inner name inside `{{name}}`. */
179
+ export const MUSTACHE_VAR_NAME_CAPTURE_REGEX = /\{\{([^}]+)\}\}/g;
180
+ /** Global with capture group: inner body inside `{#body#}`. */
181
+ export const DLT_VAR_BODY_CAPTURE_REGEX = /\{\#([^#]*)\#\}/g;
182
+
165
183
  export const GET_TRANSLATION_MAPPED = {
166
184
  'en': 'en-US',
167
185
  'zh-cn': 'zh',
@@ -199,3 +217,14 @@ export const LOGOUT_FAILURE = 'cap/LOGOUT_FAILURE';
199
217
  export const JAPANESE_HELP_TEXT = 'ヘルプ :トークンの定義';
200
218
 
201
219
  export const TAG_TRANSLATION_DOC = 'https://docs.capillarytech.com/docs/tags-translation';
220
+
221
+ // --- RCS SMS fallback API contract (shared across modules: campaigns, journey, etc.) ---
222
+
223
+ /** Keys on `messageContent.*.smsFallBackContent` sent to the API: only `message` + `templateConfigs`.
224
+ * `rcsSmsFallbackVarMapped` is editor-only — merged into `message` at normalize, not sent on the wire. */
225
+ export const RCS_API_SMS_FALLBACK_KEYS = Object.freeze({
226
+ MESSAGE: 'message',
227
+ TEMPLATE_CONFIGS: 'templateConfigs',
228
+ RCS_SMS_FALLBACK_VAR_MAPPED: 'rcsSmsFallbackVarMapped',
229
+ });
230
+
package/global-styles.js CHANGED
@@ -51,7 +51,7 @@ export default createGlobalStyle`
51
51
  }
52
52
 
53
53
  .slidebox-header-icon-right {
54
- .title-desc-container:not(:has(.whatsapp-rcs-slidebox-template-name)) {
54
+ .title-desc-container:not(:has(.whatsapp-rcs-slidebox-template-name)):not(:has(.cap-header-v2-description)) {
55
55
  display: flex;
56
56
  align-items: center;
57
57
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.29",
4
+ "version": "9.0.30",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -89,21 +89,26 @@ describe('uploadFile -- whatsapp image upload', () => {
89
89
 
90
90
  it('Uploads the file with the original filename when encodeURIComponent fails', async () => {
91
91
  // Mocking the encodeURIComponent function to throw an error
92
+ const originalEncodeURIComponent = global.encodeURIComponent;
92
93
  global.encodeURIComponent = jest.fn(() => { throw new Error('encodeURIComponent error'); });
93
94
  const blob = new Blob([''], { type: 'image/jpeg' });
94
95
  const file = new File([blob], '@%test.jpeg', { type: 'image/jpeg' });
95
- expect(
96
- uploadFile({
97
- file,
98
- assetType: 'image',
99
- fileParams: {
100
- width: 275,
101
- height: 183,
102
- error: false,
103
- },
104
- whatsappParams: {},
105
- }),
106
- ).toEqual(Promise.resolve());
96
+ try {
97
+ expect(
98
+ uploadFile({
99
+ file,
100
+ assetType: 'image',
101
+ fileParams: {
102
+ width: 275,
103
+ height: 183,
104
+ error: false,
105
+ },
106
+ whatsappParams: {},
107
+ }),
108
+ ).toEqual(Promise.resolve());
109
+ } finally {
110
+ global.encodeURIComponent = originalEncodeURIComponent;
111
+ }
107
112
  });
108
113
  });
109
114
 
@@ -1037,16 +1042,26 @@ describe('getMembersLookup', () => {
1037
1042
  expect(result).toBeInstanceOf(Promise);
1038
1043
  });
1039
1044
 
1040
- it('should call fetch with correct URL encoding and GET method', () => {
1045
+ it('should call fetch with correct URL encoding and GET method', async () => {
1041
1046
  global.fetch.mockClear();
1042
- getMembersLookup('email', 'user+test@example.com');
1047
+ await getMembersLookup('email', 'user+test@example.com');
1043
1048
  expect(global.fetch).toHaveBeenCalled();
1044
- const calls = global.fetch.mock.calls;
1045
- const anyMembersCall = calls.find((c) => c[0] && String(c[0]).includes('members'));
1046
- expect(anyMembersCall).toBeDefined();
1047
- expect(anyMembersCall[0]).toContain('identifierType=');
1048
- expect(anyMembersCall[0]).toContain('identifierValue=');
1049
- expect(anyMembersCall[1]?.method || 'GET').toBe('GET');
1049
+
1050
+ // Find the first call that uses both the members endpoint and proper encoding
1051
+ const call = global.fetch.mock.calls.find(
1052
+ ([url]) =>
1053
+ url &&
1054
+ url.includes('members') &&
1055
+ url.includes('identifierType=email') &&
1056
+ url.includes('identifierValue=user%2Btest%40example.com')
1057
+ );
1058
+ expect(call).toBeDefined();
1059
+
1060
+ // Check URL structure
1061
+ const [url, options] = call;
1062
+ expect(url).toContain('identifierType=email');
1063
+ expect(url).toContain('identifierValue=user%2Btest%40example.com');
1064
+ expect((options && options.method) || 'GET').toBe('GET');
1050
1065
  });
1051
1066
  });
1052
1067
 
@@ -539,4 +539,25 @@ 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 { templateConfigs, registeredSenderIds } = record;
552
+ const templateConfigsObject = templateConfigs && typeof templateConfigs === 'object' ? templateConfigs : {};
553
+ const {
554
+ registeredSenderIds: templateConfigRegisteredSenderIds,
555
+ header: templateConfigHeader,
556
+ } = templateConfigsObject;
557
+ const candidateSenderIdLists = [registeredSenderIds, templateConfigRegisteredSenderIds, templateConfigHeader];
558
+ for (let i = 0; i < candidateSenderIdLists.length; i += 1) {
559
+ const candidateList = candidateSenderIdLists[i];
560
+ if (Array.isArray(candidateList) && candidateList.length > 0) return candidateList;
561
+ }
562
+ return null;
563
+ }
@@ -0,0 +1,102 @@
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 varSlotValueMap = varMapped && typeof varMapped === 'object' ? varMapped : {};
17
+ if (!Object.keys(varSlotValueMap).length) return typeof templateStr === 'string' ? templateStr : '';
18
+ const safeTemplateStr = typeof templateStr === 'string' ? templateStr : '';
19
+ return splitTemplateVarString(safeTemplateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX)
20
+ .map((segment, segmentIndex) => {
21
+ if (!isAnyTemplateVarToken(segment)) return segment;
22
+ const slotKey = `${segment}_${segmentIndex}`;
23
+ if (Object.prototype.hasOwnProperty.call(varSlotValueMap, slotKey)) {
24
+ const slotValue = varSlotValueMap[slotKey];
25
+ return slotValue == null ? '' : String(slotValue);
26
+ }
27
+ return segment;
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
+ // This templateConfigs copy is not required for the API — the canonical value is recomputed
53
+ // below from mergedSmsFallbackSources and placed under smsFallBackContent.templateConfigs
54
+ // instead — so we delete it here.
55
+ delete messageContentItem.templateConfigs;
56
+ delete rcsCardPayloadOnly.templateConfigs;
57
+
58
+ if (Array.isArray(rcsCardPayloadOnly.cardContent)) {
59
+ rcsCardPayloadOnly.cardContent.forEach(card => {
60
+ if (Array.isArray(card?.suggestions)) {
61
+ card.suggestions.forEach(suggestion => {
62
+ // isSaved and urlType are UI-only editor state: campaigns' send-for-approval API
63
+ // rejects them, and they aren't part of the persisted payload shape. The dynamic tag
64
+ // (if any) already lives inline in `url` (e.g. "https://x.com{{first_name}}") — no
65
+ // separate dynamicUrlPayload field is sent for RCS.
66
+ delete suggestion?.isSaved;
67
+ delete suggestion?.urlType;
68
+ });
69
+ }
70
+ });
71
+ }
72
+
73
+ const varMappedMerged = mergedSmsFallbackSources[RCS_SMS_FALLBACK_VAR_MAPPED];
74
+ const hasVarMapped =
75
+ varMappedMerged
76
+ && typeof varMappedMerged === 'object'
77
+ && Object.keys(varMappedMerged).length > 0;
78
+ const hasMessageKey = Object.prototype.hasOwnProperty.call(mergedSmsFallbackSources, MESSAGE);
79
+ const rawMessage = hasMessageKey ? mergedSmsFallbackSources[MESSAGE] : undefined;
80
+ const messageForApi =
81
+ hasMessageKey && hasVarMapped
82
+ ? mergeSmsFallbackSlots(rawMessage == null ? '' : String(rawMessage), varMappedMerged)
83
+ : rawMessage;
84
+
85
+ const apiSiblingSmsFallback = Object.fromEntries(
86
+ [
87
+ hasMessageKey && [MESSAGE, messageForApi],
88
+ !isEmpty(mergedSmsFallbackSources[TEMPLATE_CONFIGS]) && [
89
+ TEMPLATE_CONFIGS,
90
+ mergedSmsFallbackSources[TEMPLATE_CONFIGS],
91
+ ],
92
+ ].filter(Boolean),
93
+ );
94
+
95
+ if (Object.keys(apiSiblingSmsFallback).length) {
96
+ messageContentItem.smsFallBackContent = apiSiblingSmsFallback;
97
+ } else {
98
+ // No message/templateConfigs to send, so an empty smsFallBackContent is not required for the
99
+ // API — delete any stale value already on the item rather than sending an empty container.
100
+ delete messageContentItem.smsFallBackContent;
101
+ }
102
+ };
@@ -0,0 +1,198 @@
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
+ export const isDltHashVarToken = (token) =>
20
+ typeof token === 'string' && DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX.test(token);
21
+
22
+ export const isAnyTemplateVarToken = (token) =>
23
+ (typeof token === 'string' && MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX.test(token)) || isDltHashVarToken(token);
24
+
25
+ /**
26
+ * `RegExp.prototype.exec` only advances `lastIndex` when the `g` and/or `y` flag is set.
27
+ * A non-global regex in a `while ((m = re.exec(s)) !== null)` loop never advances and can run forever.
28
+ *
29
+ * @param {RegExp} regex
30
+ * @returns {RegExp} Same instance if already global; otherwise a new RegExp with `g` appended to flags.
31
+ */
32
+ function ensureGlobalRegexForExecLoop(regex) {
33
+ if (!regex || !(regex instanceof RegExp) || regex.global) {
34
+ return regex;
35
+ }
36
+ return new RegExp(regex.source, `${regex.flags}g`);
37
+ }
38
+
39
+ /**
40
+ * Splits `content` into alternating plain-text segments and variable tokens, using the order of
41
+ * matches in `matchedVariableTokens` (e.g. from `String.prototype.match` with a global regex).
42
+ *
43
+ * @param {string[]} matchedVariableTokens - Matched tokens in left-to-right order
44
+ * @param {string} content - Full template string
45
+ * @returns {string[]}
46
+ */
47
+ export const splitContentByOrderedVarTokens = (matchedVariableTokens, content) => {
48
+ const segmentList = [];
49
+ const tokenQueue = [...(matchedVariableTokens ?? [])];
50
+ let remainder = content ?? '';
51
+ while ((remainder?.length ?? 0) > 0) {
52
+ const nextVarToken = tokenQueue?.[0];
53
+ if ([undefined, null, ''].includes(nextVarToken)) {
54
+ segmentList.push(remainder);
55
+ break;
56
+ }
57
+ const varStartIndex = remainder.indexOf(nextVarToken);
58
+ if (varStartIndex !== -1) {
59
+ segmentList.push(remainder.substring(0, varStartIndex));
60
+ segmentList.push(nextVarToken);
61
+ const afterVar = varStartIndex + (nextVarToken?.length ?? 0);
62
+ remainder = remainder.substring(afterVar, remainder?.length ?? 0);
63
+ tokenQueue.shift();
64
+ } else {
65
+ segmentList.push(remainder);
66
+ break;
67
+ }
68
+ }
69
+ return segmentList;
70
+ };
71
+
72
+ /**
73
+ * Splits a template string into an array of text + {{var}} segments using the given regex.
74
+ *
75
+ * @param {string} str - Template string
76
+ * @param {RegExp} [varRegex] - Regex that matches var tokens; defaults to DEFAULT_MUSTACHE_VAR_REGEX
77
+ * @returns {string[]}
78
+ */
79
+ export const splitTemplateVarString = (str = '', varRegex = DEFAULT_MUSTACHE_VAR_REGEX) => {
80
+ if (!str) return [];
81
+ const matchedVariableTokens = str.match(varRegex) || [];
82
+ return splitContentByOrderedVarTokens(matchedVariableTokens, str).filter(
83
+ (segment) => segment !== ''
84
+ );
85
+ };
86
+
87
+ /**
88
+ * Extracts unique variable names from `{{var}}` (and, when using default capture, `{#var#}`) strings.
89
+ *
90
+ * @param {string} templateStr
91
+ * @param {RegExp} [captureRegex] - regex with a single capture group for the var name; if omitted, also scans DLT `{#…#}`.
92
+ * @returns {string[]}
93
+ */
94
+ export const extractTemplateVariables = (templateStr = '', captureRegex) => {
95
+ if (!templateStr) return [];
96
+ const variables = [];
97
+ const addUniqueVar = (name) => {
98
+ const trimmedName = (name || '').trim();
99
+ if (trimmedName && !variables.includes(trimmedName)) variables.push(trimmedName);
100
+ };
101
+ const mustacheRe = ensureGlobalRegexForExecLoop(
102
+ captureRegex || MUSTACHE_VAR_NAME_CAPTURE_REGEX,
103
+ );
104
+ let match;
105
+ while ((match = mustacheRe.exec(templateStr)) !== null) {
106
+ addUniqueVar(match?.[1]);
107
+ }
108
+ if (!captureRegex) {
109
+ const dltRe = ensureGlobalRegexForExecLoop(DLT_VAR_BODY_CAPTURE_REGEX);
110
+ let dltMatch;
111
+ while ((dltMatch = dltRe.exec(templateStr)) !== null) {
112
+ addUniqueVar(dltMatch?.[1] || 'var');
113
+ }
114
+ }
115
+ return variables;
116
+ };
117
+
118
+ /**
119
+ * Looks up the inner name of a `{{name}}` or `{#name#}` token in a flat key→value map.
120
+ * Handles both exact matches and dot-path suffixes (e.g. `tag.FORMAT_1` → name `FORMAT_1`).
121
+ * Returns the resolved string value, or `undefined` if not found / blank.
122
+ */
123
+ function resolveUserVarForToken(segment, userVarMap) {
124
+ const mMustache = segment.match(MUSTACHE_TOKEN_INNER_NAME_REGEX);
125
+ const mDlt = segment.match(DLT_HASH_TOKEN_INNER_NAME_REGEX);
126
+ const innerName = ((mMustache ? mMustache[1] : (mDlt ? mDlt[1] : '')) ?? '').trim();
127
+ if (!innerName) return undefined;
128
+ const direct = userVarMap?.[innerName];
129
+ if (direct != null && String(direct).trim() !== '') return String(direct);
130
+ const hit = Object.keys(userVarMap ?? {}).find((key) => key === innerName || key.endsWith(`.${innerName}`));
131
+ if (hit != null && userVarMap?.[hit] != null && String(userVarMap[hit]).trim() !== '') return String(userVarMap[hit]);
132
+ return undefined;
133
+ }
134
+
135
+ /**
136
+ * SMS / DLT template preview: replace `{{…}}` / `{#…#}` tokens using `varMapData` keys `${token}_${index}`.
137
+ * Used by SmsTraiEdit (RCS SMS fallback) and UnifiedPreview fallback SMS bubble.
138
+ * DLT `{#…#}`: empty / unset slot values show the raw token (matches DLT preview UX); mustache `{{…}}` still
139
+ * resolves to empty when the slot is cleared.
140
+ *
141
+ * @param {string} templateStr
142
+ * @param {Object} varMapData - Slot map (`${token}_${index}` → value)
143
+ * @param {Object|null} [userVarMap] - Optional Test & Preview custom values; used as fallback when a slot
144
+ * is absent or blank. When omitted the function behaves identically to its original two-argument form.
145
+ */
146
+ export const getFallbackResolvedContent = (templateStr = '', varMapData = {}, userVarMap = null) => {
147
+ const fallbackVarSlotMap = varMapData ?? {};
148
+ const templateSegments = splitTemplateVarString(templateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX);
149
+ return templateSegments
150
+ .map((segment, segmentIndex) => {
151
+ const isVariableToken = typeof segment === 'string' && isAnyTemplateVarToken(segment);
152
+ if (!isVariableToken) return segment;
153
+ const slotKey = `${segment}_${segmentIndex}`;
154
+ if (Object.prototype.hasOwnProperty.call(fallbackVarSlotMap, slotKey)) {
155
+ const slotValue = fallbackVarSlotMap[slotKey];
156
+ if (isDltHashVarToken(segment)) {
157
+ if (slotValue === null || slotValue === undefined || String(slotValue).trim() === '') {
158
+ if (userVarMap) { const userVarValue = resolveUserVarForToken(segment, userVarMap); if (userVarValue !== undefined) return userVarValue; }
159
+ return segment;
160
+ }
161
+ return String(slotValue);
162
+ }
163
+ if (slotValue != null && String(slotValue).trim() !== '') return String(slotValue);
164
+ if (userVarMap) { const userVarValue = resolveUserVarForToken(segment, userVarMap); if (userVarValue !== undefined) return userVarValue; }
165
+ return '';
166
+ }
167
+ if (userVarMap) { const userVarValue = resolveUserVarForToken(segment, userVarMap); if (userVarValue !== undefined) return userVarValue; }
168
+ if (isDltHashVarToken(segment)) return segment;
169
+ return '';
170
+ })
171
+ .join('');
172
+ };
173
+
174
+ /**
175
+ * SMS fallback **card** (library list): show filled slot text; if a slot is empty, unset, or
176
+ * whitespace-only, show the raw `{{…}}` / `{#…#}` token so “save without labels” still shows
177
+ * `{#var#}` in the card. {@link getFallbackResolvedContent} keeps DLT placeholders in preview when
178
+ * slots are empty; mustache cleared slots can still render empty.
179
+ */
180
+ export const getFallbackResolvedContentForCardDisplay = (templateStr = '', varMapData = {}) => {
181
+ const fallbackVarSlotMap = varMapData ?? {};
182
+ const templateSegments = splitTemplateVarString(templateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX);
183
+ return templateSegments
184
+ .map((segment, segmentIndex) => {
185
+ const isVariableToken = typeof segment === 'string' && isAnyTemplateVarToken(segment);
186
+ if (!isVariableToken) return segment;
187
+ const slotKey = `${segment}_${segmentIndex}`;
188
+ if (Object.prototype.hasOwnProperty.call(fallbackVarSlotMap, slotKey)) {
189
+ const slotValue = fallbackVarSlotMap[slotKey];
190
+ if (slotValue == null) return segment;
191
+ const str = String(slotValue);
192
+ if (str.trim() === '') return segment;
193
+ return str;
194
+ }
195
+ return segment;
196
+ })
197
+ .join('');
198
+ };