@capillarytech/creatives-library 9.0.26 → 9.0.29-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.
Files changed (149) 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 +260 -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 +328 -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/Email/index.js +3 -54
  96. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/carouselUtils.js +224 -0
  99. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  100. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  101. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  102. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  103. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  104. package/v2Containers/Rcs/constants.js +132 -16
  105. package/v2Containers/Rcs/index.js +1808 -949
  106. package/v2Containers/Rcs/index.scss +443 -8
  107. package/v2Containers/Rcs/messages.js +45 -22
  108. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +484 -0
  109. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  110. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  111. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  112. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  113. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  114. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  115. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  116. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  117. package/v2Containers/Rcs/tests/index.test.js +219 -40
  118. package/v2Containers/Rcs/tests/mockData.js +38 -0
  119. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
  120. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  121. package/v2Containers/Rcs/utils.js +514 -12
  122. package/v2Containers/Sms/Create/index.js +115 -48
  123. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  124. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  125. package/v2Containers/SmsTrai/Create/index.js +9 -4
  126. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  127. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  128. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  129. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  130. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  131. package/v2Containers/SmsWrapper/index.js +37 -8
  132. package/v2Containers/TagList/index.js +5 -0
  133. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  134. package/v2Containers/Templates/_templates.scss +196 -12
  135. package/v2Containers/Templates/actions.js +11 -0
  136. package/v2Containers/Templates/constants.js +2 -0
  137. package/v2Containers/Templates/index.js +131 -59
  138. package/v2Containers/Templates/sagas.js +57 -13
  139. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  140. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  141. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  142. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  143. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  144. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  145. package/v2Containers/TemplatesV2/index.js +88 -25
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  147. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  148. package/v2Containers/Whatsapp/index.js +3 -20
  149. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,484 @@
1
+ import isEmpty from 'lodash/isEmpty';
2
+ import get from 'lodash/get';
3
+ import { extractRegisteredSenderIdsFromSmsFallbackRecord } from '../../utils/commonUtils';
4
+ import { isTraiDLTEnable } from '../../utils/common';
5
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
6
+ import {
7
+ RCS_NUMERIC_VAR_NAME_REGEX,
8
+ RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX,
9
+ REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
10
+ RCS_MUSTACHE_WRAPPED_REGEX,
11
+ } from './constants';
12
+
13
+ /** RCS card `cardVarMapped` / `rcsCardVarMapped` only (SMS fallback slot keys stay on `smsFallBackContent`). */
14
+ export function pickRcsCardVarMappedEntries(record) {
15
+ if (record == null || typeof record !== 'object' || Array.isArray(record)) return {};
16
+ return Object.fromEntries(
17
+ Object.entries(record).filter(([entryKey]) => {
18
+ const key = String(entryKey);
19
+ return (
20
+ RCS_NUMERIC_VAR_NAME_REGEX.test(key)
21
+ || RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX.test(key)
22
+ );
23
+ }),
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Nested `versions…smsFallBackContent` and root `smsFallBackContent` (CreativesContainer mirror)
29
+ * can diverge. Merge explicitly (avoid `get(..., {}) || root` — `{}` is truthy and blocks fallback).
30
+ *
31
+ * When both exist, **nested wins on overlap**: the canonical RCS path is what `createPayload` / submit
32
+ * update; the root mirror can lag behind campaign parent state and would otherwise keep old
33
+ * `message` / `rcsSmsFallbackVarMapped` in hydration and approval payloads.
34
+ */
35
+ export function mergeRcsSmsFallBackContentFromDetails(details) {
36
+ if (!details || typeof details !== 'object') return {};
37
+ const nestedSmsFallback = details?.versions?.base?.content?.RCS?.smsFallBackContent;
38
+ const rootSmsFallback = details?.smsFallBackContent;
39
+ const nestedRecord =
40
+ nestedSmsFallback != null && typeof nestedSmsFallback === 'object' ? nestedSmsFallback : {};
41
+ const rootRecord =
42
+ rootSmsFallback != null && typeof rootSmsFallback === 'object' ? rootSmsFallback : {};
43
+ return { ...rootRecord, ...nestedRecord };
44
+ }
45
+
46
+ /**
47
+ * Merge SMS fallback slot maps from API/templateData (`apiShape`) and editor state (`localShape`).
48
+ * Spreading `{ ...api, ...local }` lets `rcsSmsFallbackVarMapped: {}` on local wipe a populated API map,
49
+ * which keeps Done disabled in DLT campaigns until fixed.
50
+ */
51
+ export function mergeRcsSmsFallbackVarMapLayers(apiShape = {}, localShape = {}) {
52
+ const readMap = (shape) => {
53
+ if (!shape || typeof shape !== 'object') return {};
54
+ const camel = shape.rcsSmsFallbackVarMapped;
55
+ const kebab = shape['rcs-sms-fallback-var-mapped'];
56
+ const src =
57
+ camel && typeof camel === 'object'
58
+ ? camel
59
+ : kebab && typeof kebab === 'object'
60
+ ? kebab
61
+ : {};
62
+ return { ...src };
63
+ };
64
+ return { ...readMap(apiShape), ...readMap(localShape) };
65
+ }
66
+
67
+ /**
68
+ * First non-empty trimmed string for SMS fallback body used in DLT slot checks / payload `templateContent`.
69
+ * Prefer **raw** template fields before resolved `message` so `{#…#}` / `{{…}}` tokens are not lost
70
+ * when `message` is consumer-resolved text (campaigns).
71
+ */
72
+ /**
73
+ * Whether an SMS fallback object carries template body, name, or var-map data (same rule as Rcs `createPayload`).
74
+ */
75
+ export function hasMeaningfulSmsFallbackShape(smsFallback) {
76
+ return !!(
77
+ smsFallback
78
+ && (
79
+ String(
80
+ smsFallback.content
81
+ || smsFallback.templateContent
82
+ || smsFallback.message
83
+ || smsFallback.smsContent
84
+ || smsFallback.smsTemplateContent
85
+ || '',
86
+ ).trim() !== ''
87
+ || String(smsFallback.templateName || smsFallback.smsTemplateName || '').trim() !== ''
88
+ || (smsFallback.rcsSmsFallbackVarMapped && Object.keys(smsFallback.rcsSmsFallbackVarMapped).length > 0)
89
+ || (smsFallback[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
90
+ && Object.keys(smsFallback[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0)
91
+ )
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Library `templateData`: merged root + nested `smsFallBackContent` (nested wins), matching `createPayload`.
97
+ */
98
+ export function getLibrarySmsFallbackApiBaselineFromTemplateData(templateData) {
99
+ const smsFromTemplateRoot = get(
100
+ templateData,
101
+ 'versions.base.content.RCS.smsFallBackContent',
102
+ );
103
+ return {
104
+ ...(templateData?.smsFallBackContent && typeof templateData.smsFallBackContent === 'object'
105
+ ? templateData.smsFallBackContent
106
+ : {}),
107
+ ...(smsFromTemplateRoot && typeof smsFromTemplateRoot === 'object'
108
+ ? smsFromTemplateRoot
109
+ : {}),
110
+ };
111
+ }
112
+
113
+ export { extractRegisteredSenderIdsFromSmsFallbackRecord } from '../../utils/commonUtils';
114
+
115
+ /**
116
+ * Library/campaign: merge API baseline (`templateData`) with local editor state (`smsFallbackData`).
117
+ * Full mode has no API baseline to merge against — Send-for-approval reads editor state as-is.
118
+ * Shared by `createPayload`'s payload merge and Done/slot validation so both read the same merge
119
+ * and can't drift apart.
120
+ */
121
+ export function mergeSmsFallbackForLibrary(templateData, smsFallbackData, isFullMode) {
122
+ if (isFullMode) {
123
+ return smsFallbackData || {};
124
+ }
125
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
126
+ const local =
127
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
128
+ return {
129
+ ...smsFromApiShape,
130
+ ...local,
131
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Build the `smsFallBackContent` object for the RCS template save/edit payload, or `undefined`
137
+ * when there is no meaningful SMS fallback to persist. Mirrors `createPayload`'s two shapes:
138
+ * full mode (Send for approval) only carries the fields cap-campaigns-v2's
139
+ * `normalizeRcsMessageContentForApi` reads (`message` + `templateConfigs`); library/campaign mode
140
+ * also stores template name/content/var-map/sender-ids so reopening the editor can rehydrate.
141
+ */
142
+ export function buildSmsFallBackContentForPayload({
143
+ smsFallbackData,
144
+ templateData,
145
+ isFullMode,
146
+ smsRegister,
147
+ }) {
148
+ const smsFallbackMerged = mergeSmsFallbackForLibrary(templateData, smsFallbackData, isFullMode);
149
+ const smsFallbackForPayload = isFullMode
150
+ ? (hasMeaningfulSmsFallbackShape(smsFallbackData) ? smsFallbackData : null)
151
+ : (() => {
152
+ const mapped = {
153
+ templateName:
154
+ smsFallbackMerged.templateName
155
+ || smsFallbackMerged.smsTemplateName
156
+ || '',
157
+ // Use `||` so empty `content` does not block campaign/API `message` (common in embedded flows).
158
+ content:
159
+ smsFallbackMerged.content
160
+ || smsFallbackMerged.smsContent
161
+ || smsFallbackMerged.smsTemplateContent
162
+ || smsFallbackMerged.message
163
+ || '',
164
+ templateContent:
165
+ pickFirstSmsFallbackTemplateString(smsFallbackMerged)
166
+ || '',
167
+ ...(typeof smsFallbackMerged.unicodeValidity === 'boolean'
168
+ && { unicodeValidity: smsFallbackMerged.unicodeValidity }),
169
+ ...(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
170
+ && Object.keys(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
171
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
172
+ smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
173
+ }),
174
+ };
175
+ return hasMeaningfulSmsFallbackShape(mapped) ? mapped : null;
176
+ })();
177
+
178
+ if (!smsFallbackForPayload) return undefined;
179
+
180
+ const smsBodyText =
181
+ smsFallbackForPayload.content
182
+ || smsFallbackForPayload.templateContent
183
+ || smsFallbackForPayload.message
184
+ || smsFallbackForPayload.smsContent
185
+ || '';
186
+ /**
187
+ * Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
188
+ * Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
189
+ */
190
+ const mergedFallback = smsFallbackMerged || {};
191
+ const tcSibling = mergedFallback.templateConfigs && typeof mergedFallback.templateConfigs === 'object'
192
+ ? mergedFallback.templateConfigs
193
+ : {};
194
+ const smsFallbackTemplateId =
195
+ (mergedFallback.smsTemplateId != null && String(mergedFallback.smsTemplateId).trim() !== ''
196
+ ? String(mergedFallback.smsTemplateId)
197
+ : '')
198
+ || (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
199
+ ? String(tcSibling.templateId)
200
+ : '');
201
+ const smsFallbackTemplateStr =
202
+ pickFirstSmsFallbackTemplateString(mergedFallback)
203
+ || (typeof mergedFallback.templateContent === 'string' ? mergedFallback.templateContent : '')
204
+ || (typeof tcSibling.template === 'string' ? tcSibling.template : '')
205
+ || '';
206
+ const smsFallbackTemplateName =
207
+ mergedFallback.templateName
208
+ || mergedFallback.smsTemplateName
209
+ || tcSibling.templateName
210
+ || tcSibling.name
211
+ || '';
212
+ const registeredSenderIdsForPayload = Array.isArray(mergedFallback.registeredSenderIds)
213
+ ? mergedFallback.registeredSenderIds
214
+ : Array.isArray(tcSibling.registeredSenderIds)
215
+ ? tcSibling.registeredSenderIds
216
+ : Array.isArray(tcSibling.header)
217
+ ? tcSibling.header
218
+ : null;
219
+ const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
220
+ const smsFallbackTemplateConfigs =
221
+ smsFallbackTemplateId || hasRegisteredSenderIds
222
+ ? {
223
+ ...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
224
+ ...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
225
+ ...(smsFallbackTemplateName && {
226
+ templateName: smsFallbackTemplateName,
227
+ }),
228
+ ...(hasRegisteredSenderIds && {
229
+ registeredSenderIds: registeredSenderIdsForPayload,
230
+ }),
231
+ }
232
+ : null;
233
+ const isDltCampaign = !isFullMode && isTraiDLTEnable(isFullMode, smsRegister);
234
+
235
+ const smsFallBackContent = isFullMode
236
+ ? {
237
+ smsTemplateName: smsFallbackForPayload.templateName || '',
238
+ smsContent: smsBodyText,
239
+ // cap-campaigns-v2 `normalizeRcsMessageContentForApi` only serializes `message` (+ templateConfigs); without this key SMS fallback is dropped on send.
240
+ message: smsBodyText,
241
+ ...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
242
+ unicodeValidity: smsFallbackForPayload.unicodeValidity,
243
+ }),
244
+ ...(smsFallbackForPayload.rcsSmsFallbackVarMapped
245
+ && Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
246
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
247
+ }),
248
+ ...(smsFallbackTemplateConfigs && {
249
+ templateConfigs: smsFallbackTemplateConfigs,
250
+ }),
251
+ }
252
+ : {
253
+ // Round-trip storage: full shape so reopening the editor restores template
254
+ // name, sender IDs, and var mappings. normalizeRcsMessageContentForApi
255
+ // (called in CreativesContainer getCreativesData) strips this to
256
+ // { message, templateConfigs } before the API call — the extra fields here
257
+ // are only used for re-hydration, never sent to the API.
258
+ message: smsBodyText,
259
+ ...(smsFallbackForPayload.templateName && {
260
+ smsTemplateName: smsFallbackForPayload.templateName,
261
+ }),
262
+ ...(smsFallbackForPayload.templateContent && {
263
+ templateContent: smsFallbackForPayload.templateContent,
264
+ }),
265
+ ...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
266
+ unicodeValidity: smsFallbackForPayload.unicodeValidity,
267
+ }),
268
+ ...(Array.isArray(registeredSenderIdsForPayload) && {
269
+ registeredSenderIds: registeredSenderIdsForPayload,
270
+ }),
271
+ ...(smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
272
+ && Object.keys(smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
273
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
274
+ smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
275
+ }),
276
+ ...(isDltCampaign && smsFallbackTemplateConfigs && {
277
+ templateConfigs: smsFallbackTemplateConfigs,
278
+ }),
279
+ };
280
+
281
+ return { smsFallBackContent };
282
+ }
283
+
284
+ /**
285
+ * Resolve SMS-fallback hydration state from RCS template `details` — shared by the carousel and
286
+ * non-carousel hydration branches in Rcs/index.js so template shape doesn't fork the fallback-message
287
+ * / unicode / var-map / sender-id resolution logic. `hydrationKey` is a stable fingerprint used to
288
+ * skip a redundant `setSmsFallbackData` when re-hydrating with unchanged data.
289
+ */
290
+ export function resolveSmsFallbackHydrationFromDetails(details) {
291
+ const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
292
+ const base = get(smsFallbackContent, 'versions.base', {});
293
+ const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
294
+ const smsEditor = base['sms-editor'];
295
+ const fromNested = Array.isArray(updatedEditor)
296
+ ? updatedEditor.join('')
297
+ : (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
298
+ const fallbackMessage = smsFallbackContent.smsContent
299
+ || smsFallbackContent.smsTemplateContent
300
+ || smsFallbackContent.message
301
+ || fromNested
302
+ || '';
303
+ const varMappedFromPayload = smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
304
+ const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
305
+ const hasFallbackPayload =
306
+ smsFallbackContent
307
+ && Object.keys(smsFallbackContent).length > 0
308
+ && (
309
+ !!smsFallbackContent.smsTemplateName
310
+ || !!fallbackMessage
311
+ || hasVarMapped
312
+ );
313
+
314
+ if (!hasFallbackPayload) {
315
+ return { hasFallbackPayload: false, nextSmsState: null, hydrationKey: '__EMPTY__' };
316
+ }
317
+
318
+ if (!fallbackMessage && !hasVarMapped && process.env.NODE_ENV !== 'production') {
319
+ // eslint-disable-next-line no-console
320
+ console.warn('[RCS SMS Fallback] No message text found in API response. Inspect shape:', smsFallbackContent);
321
+ }
322
+ const unicodeFromApi =
323
+ typeof smsFallbackContent.unicodeValidity === 'boolean'
324
+ ? smsFallbackContent.unicodeValidity
325
+ : (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
326
+ const registeredSenderIds = extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
327
+ const nextSmsState = {
328
+ templateName: smsFallbackContent.smsTemplateName || '',
329
+ content: fallbackMessage,
330
+ templateContent: fallbackMessage,
331
+ unicodeValidity: unicodeFromApi,
332
+ ...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
333
+ ...(Array.isArray(registeredSenderIds) && registeredSenderIds.length > 0
334
+ ? { registeredSenderIds }
335
+ : {}),
336
+ };
337
+ const hydrationKey = JSON.stringify({
338
+ creativeKey: details._id || details.name || details.creativeName || '',
339
+ templateName: nextSmsState.templateName,
340
+ content: nextSmsState.content,
341
+ unicodeValidity: nextSmsState.unicodeValidity,
342
+ varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
343
+ senderIds: Array.isArray(registeredSenderIds) ? registeredSenderIds.join('') : '',
344
+ });
345
+ return { hasFallbackPayload: true, nextSmsState, hydrationKey };
346
+ }
347
+
348
+ /**
349
+ * Apply SMS-fallback hydration to component state — shared by the carousel and non-carousel
350
+ * hydration branches in Rcs/index.js so both stay in sync on the ref-guard / setSmsFallbackData logic.
351
+ */
352
+ export function applySmsFallbackHydration({
353
+ details,
354
+ isFullMode,
355
+ lastSmsFallbackHydrationKeyRef,
356
+ setSmsFallbackData,
357
+ }) {
358
+ const smsFallback = resolveSmsFallbackHydrationFromDetails(details);
359
+ if (smsFallback.hasFallbackPayload) {
360
+ if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== smsFallback.hydrationKey) {
361
+ lastSmsFallbackHydrationKeyRef.current = smsFallback.hydrationKey;
362
+ setSmsFallbackData(smsFallback.nextSmsState);
363
+ }
364
+ } else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
365
+ lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
366
+ setSmsFallbackData(null);
367
+ }
368
+ }
369
+
370
+ export function pickFirstSmsFallbackTemplateString(sms = {}) {
371
+ if (!sms || typeof sms !== 'object') return '';
372
+ const keys = [
373
+ 'templateContent',
374
+ 'smsTemplateContent',
375
+ 'content',
376
+ 'smsContent',
377
+ 'message',
378
+ ];
379
+ for (let i = 0; i < keys.length; i += 1) {
380
+ const fieldValue = sms[keys[i]];
381
+ if (fieldValue == null) continue;
382
+ const trimmedValue = String(fieldValue).trim();
383
+ if (trimmedValue) return trimmedValue;
384
+ }
385
+ return '';
386
+ }
387
+
388
+ /**
389
+ * Campaign reopen: payloads often keep tag/label values on numeric keys (`"1"`, `"2"`, …) while
390
+ * semantic keys (`user_name`, …) stay `""`. Copy non-empty slot values onto empty semantic keys so
391
+ * VarSegment editors prepopulate after Done → Edit without changing global resolve behavior.
392
+ */
393
+ export function syncCardVarMappedSemanticsFromSlots(
394
+ cardVarMappedInput,
395
+ templateTitle,
396
+ templateDesc,
397
+ rcsVarRegex,
398
+ ) {
399
+ const cardVarMappedSynced =
400
+ cardVarMappedInput != null && typeof cardVarMappedInput === 'object'
401
+ ? { ...cardVarMappedInput }
402
+ : {};
403
+ const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '').trim();
404
+ const templateVarTokens = [
405
+ ...(templateTitle?.match(rcsVarRegex) ?? []),
406
+ ...(templateDesc?.match(rcsVarRegex) ?? []),
407
+ ];
408
+ templateVarTokens.forEach((token, slotIndexZeroBased) => {
409
+ const semanticVarName = getVarNameFromToken(token);
410
+ if (!semanticVarName) return;
411
+ const numericSlotKey = String(slotIndexZeroBased + 1);
412
+ const semanticValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
413
+ const numericSlotValueTrimmed = String(cardVarMappedSynced[numericSlotKey] ?? '').trim();
414
+ if (!semanticValueTrimmed && numericSlotValueTrimmed) {
415
+ cardVarMappedSynced[semanticVarName] = cardVarMappedSynced[numericSlotKey];
416
+ }
417
+ });
418
+ return cardVarMappedSynced;
419
+ }
420
+
421
+ /**
422
+ * Maps resolved campaign values back to `{{semanticKey}}` using `cardVarMapped` keys.
423
+ * Used when hydrating library/journey RCS edit from payloads without existing `{{…}}` tokens.
424
+ */
425
+ export function getUnmappedDesc(str, mapping) {
426
+ if (!str) return '';
427
+ if (!mapping || Object.keys(mapping).length === 0) return str;
428
+ let result = str;
429
+ const replacements = [];
430
+ Object.entries(mapping).forEach(([key, value]) => {
431
+ const raw = (value ?? '').toString();
432
+ if (!raw || raw?.trim?.() === '') return;
433
+ const braced = RCS_MUSTACHE_WRAPPED_REGEX.test(raw) ? raw : `{{${raw}}}`;
434
+ replacements.push({ key, needle: raw });
435
+ if (braced !== raw) replacements.push({ key, needle: braced });
436
+ });
437
+ const seen = new Set();
438
+ const uniq = replacements
439
+ .filter(({ key, needle }) => {
440
+ const id = `${key}::${needle}`;
441
+ if (seen.has(id)) return false;
442
+ seen.add(id);
443
+ return true;
444
+ })
445
+ .sort((a, b) => (b.needle.length - a.needle.length));
446
+
447
+ uniq.forEach(({ key, needle }) => {
448
+ if (!needle) return;
449
+ const escaped = needle.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
450
+ const regex = new RegExp(escaped, 'g');
451
+ result = result.replace(regex, `{{${key}}}`);
452
+ });
453
+ return result;
454
+ }
455
+
456
+ export function hasRcsVarTokens(str, rcsVarRegex) {
457
+ return (str?.match(rcsVarRegex) ?? []).length > 0;
458
+ }
459
+
460
+ /**
461
+ * Library / journey: only run `getUnmappedDesc` when the loaded copy has no `{{…}}` tokens
462
+ * (e.g. fully resolved campaign text). If the API already has numeric or named slots, keep as-is.
463
+ */
464
+ export function normalizeLibraryLoadedTitleDesc({
465
+ loadedTitle,
466
+ loadedDesc,
467
+ isFullMode,
468
+ cardVarMappedAfterHydration,
469
+ rcsVarRegex,
470
+ }) {
471
+ const normalizedTitle =
472
+ !isFullMode
473
+ && !isEmpty(cardVarMappedAfterHydration)
474
+ && !hasRcsVarTokens(loadedTitle, rcsVarRegex)
475
+ ? getUnmappedDesc(loadedTitle, cardVarMappedAfterHydration)
476
+ : loadedTitle;
477
+ const normalizedDesc =
478
+ !isFullMode
479
+ && !isEmpty(cardVarMappedAfterHydration)
480
+ && !hasRcsVarTokens(loadedDesc, rcsVarRegex)
481
+ ? getUnmappedDesc(loadedDesc, cardVarMappedAfterHydration)
482
+ : loadedDesc;
483
+ return { normalizedTitle, normalizedDesc };
484
+ }