@capillarytech/creatives-library 8.0.319 → 8.0.320

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 (106) hide show
  1. package/constants/unified.js +14 -0
  2. package/package.json +1 -1
  3. package/utils/templateVarUtils.js +172 -0
  4. package/utils/tests/templateVarUtils.test.js +160 -0
  5. package/v2Components/CapTagList/index.js +10 -0
  6. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  7. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  8. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  9. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  13. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  15. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  16. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  17. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  18. package/v2Components/CommonTestAndPreview/index.js +693 -155
  19. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  20. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  21. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  22. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  23. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  24. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  25. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  26. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  27. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  28. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  29. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  30. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  31. package/v2Components/FormBuilder/index.js +7 -1
  32. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  33. package/v2Components/SmsFallback/constants.js +73 -0
  34. package/v2Components/SmsFallback/index.js +956 -0
  35. package/v2Components/SmsFallback/index.scss +265 -0
  36. package/v2Components/SmsFallback/messages.js +78 -0
  37. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  38. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  39. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  40. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  41. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  42. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  43. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  44. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  45. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  46. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  47. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  48. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  49. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  50. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  51. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  52. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  53. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  54. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  55. package/v2Containers/CreativesContainer/constants.js +9 -0
  56. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  57. package/v2Containers/CreativesContainer/index.js +289 -99
  58. package/v2Containers/CreativesContainer/index.scss +51 -1
  59. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  60. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  61. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  62. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  63. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  64. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  65. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  66. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  67. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  68. package/v2Containers/Rcs/constants.js +32 -1
  69. package/v2Containers/Rcs/index.js +950 -873
  70. package/v2Containers/Rcs/index.scss +85 -6
  71. package/v2Containers/Rcs/messages.js +10 -1
  72. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  73. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  74. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  75. package/v2Containers/Rcs/tests/index.test.js +41 -38
  76. package/v2Containers/Rcs/tests/mockData.js +38 -0
  77. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  78. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  79. package/v2Containers/Rcs/utils.js +358 -10
  80. package/v2Containers/Sms/Create/index.js +81 -36
  81. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  82. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  83. package/v2Containers/SmsTrai/Create/index.js +9 -4
  84. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  85. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  86. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  87. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  88. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  89. package/v2Containers/SmsWrapper/index.js +37 -8
  90. package/v2Containers/TagList/index.js +6 -0
  91. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  92. package/v2Containers/Templates/_templates.scss +61 -2
  93. package/v2Containers/Templates/actions.js +11 -0
  94. package/v2Containers/Templates/constants.js +2 -0
  95. package/v2Containers/Templates/index.js +90 -40
  96. package/v2Containers/Templates/sagas.js +57 -12
  97. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  98. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  99. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  100. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  101. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  102. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  103. package/v2Containers/TemplatesV2/index.js +86 -23
  104. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  105. package/v2Containers/Whatsapp/index.js +3 -20
  106. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -1,11 +1,27 @@
1
1
  import React from 'react';
2
2
  import {CapIcon, CapImage, CapLabel, CapDivider } from '@capillarytech/cap-ui-library';
3
- import { RCS } from './constants';
3
+ import {
4
+ RCS,
5
+ RCS_MEDIA_TYPES,
6
+ RCS_NUMERIC_VAR_NAME_REGEX,
7
+ RCS_REGEX_META_CHARS_PATTERN,
8
+ RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
9
+ } from './constants';
4
10
  import './index.scss';
5
11
  // import { formatMessage } from '../../../utils/intl';
6
12
  import messages from './messages';
7
- import { STATUS_OPTIONS, RCS_BUTTON_TYPES, RCS_STATUSES, RCS_MEDIA_TYPES } from './constants';
8
-
13
+ import {
14
+ STATUS_OPTIONS,
15
+ RCS_BUTTON_TYPES,
16
+ RCS_STATUSES,
17
+ rcsVarRegex,
18
+ rcsVarTestRegex,
19
+ } from './constants';
20
+ import {
21
+ splitTemplateVarString,
22
+ COMBINED_SMS_TEMPLATE_VAR_REGEX,
23
+ isAnyTemplateVarToken,
24
+ } from '../../utils/templateVarUtils';
9
25
 
10
26
  export const getRcsStatusType = (status) => {
11
27
  switch (status) {
@@ -33,6 +49,334 @@ export const getTemplateStatusType = (templateStatus) => {
33
49
  }
34
50
  };
35
51
 
52
+ /**
53
+ * Global RegExp matching `{{numericVarName}}` in RCS template strings.
54
+ * `numericVarName` is escaped for regex metacharacters.
55
+ */
56
+ export function buildRcsNumericMustachePlaceholderRegex(numericVarName) {
57
+ const escaped = String(numericVarName).replace(RCS_REGEX_META_CHARS_PATTERN, '\\$&');
58
+ return new RegExp(`\\{\\{${escaped}\\}\\}`, 'g');
59
+ }
60
+
61
+ export function normalizeCardVarMapped(rawCardVarMapped, orderedTagNames) {
62
+ if (!rawCardVarMapped || typeof rawCardVarMapped !== 'object') return {};
63
+ const normalizedMap = {};
64
+ const templateVarNamesInOrder = Array.isArray(orderedTagNames) ? orderedTagNames : null;
65
+ const hasOrderedSlots =
66
+ Boolean(templateVarNamesInOrder?.length);
67
+ Object.entries(rawCardVarMapped).forEach(([entryKey, entryValue]) => {
68
+ const trimmedValue = entryValue == null ? '' : String(entryValue).trim();
69
+ const entryKeyIsNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(entryKey));
70
+ const mustacheInnerMatch = trimmedValue.match(/^\{\{([^}]+)\}\}$/);
71
+ const innerFromMustache =
72
+ mustacheInnerMatch?.[1] != null ? String(mustacheInnerMatch[1]).trim() : null;
73
+
74
+ if (innerFromMustache !== null && entryKeyIsNumericSlot) {
75
+ const slotIndexZeroBased = parseInt(String(entryKey), 10) - 1;
76
+ const expectedVarForSlot =
77
+ hasOrderedSlots
78
+ && slotIndexZeroBased >= 0
79
+ && slotIndexZeroBased < templateVarNamesInOrder.length
80
+ ? templateVarNamesInOrder[slotIndexZeroBased]
81
+ : null;
82
+ const innerMatchesSlotToken =
83
+ expectedVarForSlot != null && innerFromMustache === expectedVarForSlot;
84
+ const legacyUnorderedPlaceholder = !hasOrderedSlots;
85
+ /* Library: slot "1" + {{user_id_b64}} when token is user_id_b64 → empty semantic. With ordered
86
+ * slots, only clear when inner matches that slot's template token; else keep (e.g. {{1}}+{{FirstName}}). */
87
+ const clearNumericSlotMustacheAsUnfilled =
88
+ !RCS_NUMERIC_VAR_NAME_REGEX.test(innerFromMustache)
89
+ && (legacyUnorderedPlaceholder || innerMatchesSlotToken);
90
+ if (clearNumericSlotMustacheAsUnfilled) {
91
+ const outputKey = innerFromMustache;
92
+ const existingValue = normalizedMap[outputKey];
93
+ if (existingValue != null && String(existingValue).trim() !== '') {
94
+ return;
95
+ }
96
+ normalizedMap[outputKey] = '';
97
+ return;
98
+ }
99
+ if (RCS_NUMERIC_VAR_NAME_REGEX.test(innerFromMustache)) {
100
+ const outputKey = innerFromMustache;
101
+ const existingValue = normalizedMap[outputKey];
102
+ if (existingValue != null && String(existingValue).trim() !== '') {
103
+ return;
104
+ }
105
+ normalizedMap[outputKey] = '';
106
+ return;
107
+ }
108
+ }
109
+
110
+ if (innerFromMustache !== null && !entryKeyIsNumericSlot) {
111
+ if (innerFromMustache === String(entryKey)) {
112
+ const existingValue = normalizedMap[entryKey];
113
+ if (existingValue != null && String(existingValue).trim() !== '') {
114
+ return;
115
+ }
116
+ normalizedMap[entryKey] = '';
117
+ return;
118
+ }
119
+ }
120
+
121
+ if (entryKeyIsNumericSlot && templateVarNamesInOrder?.length) {
122
+ const slotIndexZeroBased = parseInt(String(entryKey), 10) - 1;
123
+ if (slotIndexZeroBased >= 0 && slotIndexZeroBased < templateVarNamesInOrder.length) {
124
+ normalizedMap[templateVarNamesInOrder[slotIndexZeroBased]] = trimmedValue;
125
+ return;
126
+ }
127
+ }
128
+ normalizedMap[entryKey] = trimmedValue;
129
+ });
130
+ return normalizedMap;
131
+ }
132
+
133
+ /**
134
+ * Rebuild `cardVarMapped` so keys match the current title/description tokens (title tokens first,
135
+ * then description), in order. Values are taken from the matching key, else from legacy slot
136
+ * `1`, `2`, … by index. If there are no `{{...}}` tokens, returns a shallow clone of `raw`.
137
+ */
138
+ export function coalesceCardVarMappedToTemplate(
139
+ sourceCardVarMap,
140
+ templateTitle,
141
+ templateDesc,
142
+ rcsVarRegex,
143
+ ) {
144
+ const getVarNameFromToken = (token = '') => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
145
+ const templateVarTokens = [
146
+ ...(templateTitle?.match(rcsVarRegex) ?? []),
147
+ ...(templateDesc?.match(rcsVarRegex) ?? []),
148
+ ];
149
+ const lookupSourceMap =
150
+ sourceCardVarMap != null && typeof sourceCardVarMap === 'object' ? sourceCardVarMap : {};
151
+ if (!templateVarTokens.length) {
152
+ return { ...lookupSourceMap };
153
+ }
154
+ const coalescedMap = { ...lookupSourceMap };
155
+ const seenSemanticVarNames = new Set();
156
+ templateVarTokens.forEach((token, slotIndexZeroBased) => {
157
+ const semanticVarName = getVarNameFromToken(token);
158
+ if (!semanticVarName) return;
159
+ const numericSlotKey = String(slotIndexZeroBased + 1);
160
+ let valueFromSource = lookupSourceMap[numericSlotKey];
161
+ if (valueFromSource === undefined || valueFromSource === null) {
162
+ valueFromSource = lookupSourceMap[semanticVarName];
163
+ }
164
+ if (valueFromSource === undefined || valueFromSource === null) {
165
+ valueFromSource = lookupSourceMap[String(slotIndexZeroBased + 1)];
166
+ }
167
+ if (valueFromSource === undefined || valueFromSource === null) {
168
+ valueFromSource = lookupSourceMap[slotIndexZeroBased + 1];
169
+ }
170
+ const trimmedSlotValue = valueFromSource == null ? '' : String(valueFromSource).trim();
171
+ coalescedMap[numericSlotKey] = trimmedSlotValue;
172
+ if (!seenSemanticVarNames.has(semanticVarName)) {
173
+ seenSemanticVarNames.add(semanticVarName);
174
+ coalescedMap[semanticVarName] = trimmedSlotValue;
175
+ }
176
+ });
177
+ return coalescedMap;
178
+ }
179
+
180
+ /**
181
+ * Resolve the personalization value for a variable slot — aligned with createPayload:
182
+ * per-slot numeric keys `1`, `2`, … win over legacy semantic keys when both exist (duplicate
183
+ * `{{name}}` in title+desc). If semantic is explicitly cleared to '', that still wins over a
184
+ * stale numeric value (see tests) — except in embedded library / journey mode (`isLibraryMode`).
185
+ *
186
+ * In library mode, campaign payloads often set semantic keys to '' while numeric slot `1`,`2`,…
187
+ * still holds the value selected in the library; prefer that so VarSegment prepopulates.
188
+ *
189
+ * When a numeric slot is present but only whitespace / empty (common after hydration), do not
190
+ * treat it as authoritative — fall through to the semantic key so preview and payload match the
191
+ * tag the user selected (e.g. `1: ''` but `promotion_points: '{{newTag}}'`).
192
+ */
193
+ export function resolveCardVarMappedSlotValue(
194
+ cardVarMapped,
195
+ varName,
196
+ globalSlotIndexZeroBased,
197
+ isLibraryMode = false,
198
+ ) {
199
+ const varMap = cardVarMapped ?? {};
200
+ const slotKey = String(globalSlotIndexZeroBased + 1);
201
+ const semanticEmpty =
202
+ Object.prototype.hasOwnProperty.call(varMap, varName)
203
+ && String(varMap[varName] ?? '') === '';
204
+ const slotNonEmpty =
205
+ Object.prototype.hasOwnProperty.call(varMap, slotKey)
206
+ && String(varMap[slotKey] ?? '').trim() !== '';
207
+
208
+ if (semanticEmpty && !(isLibraryMode && slotNonEmpty)) {
209
+ return '';
210
+ }
211
+ let numericSlotValue = '';
212
+ if (Object.prototype.hasOwnProperty.call(varMap, slotKey)) {
213
+ numericSlotValue = String(varMap[slotKey] ?? '');
214
+ } else if (Object.prototype.hasOwnProperty.call(varMap, globalSlotIndexZeroBased + 1)) {
215
+ numericSlotValue = String(varMap[globalSlotIndexZeroBased + 1] ?? '');
216
+ }
217
+ if (numericSlotValue.trim() !== '') {
218
+ return numericSlotValue;
219
+ }
220
+ if (Object.prototype.hasOwnProperty.call(varMap, varName)) {
221
+ return String(varMap[varName] ?? '');
222
+ }
223
+ return '';
224
+ }
225
+
226
+ /** Text-only RCS card: editor shows a single “Text message” field (description); title row is hidden. */
227
+ export function isRcsTextOnlyCardMediaType(mediaType) {
228
+ return (
229
+ mediaType === RCS_MEDIA_TYPES.NONE
230
+ || String(mediaType || '').toUpperCase() === 'TEXT'
231
+ );
232
+ }
233
+
234
+ /**
235
+ * Resolve RCS card title/description for TemplatePreview (e.g. campaign slidebox preview).
236
+ * Mirrors `resolveTemplateWithMap` in the Rcs editor: title vars use global slots 0..n-1, then description.
237
+ * For text-only cards (`textOnlyCard`), ignore persisted `title` and resolve description from slot 0 — matches
238
+ * the editor where only the message body is shown.
239
+ */
240
+ export function resolveRcsCardPreviewStrings(
241
+ title,
242
+ description,
243
+ cardVarMapped,
244
+ isLibraryMode = false,
245
+ textOnlyCard = false,
246
+ ) {
247
+ const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
248
+ const getVarNameFromToken = (token = '') =>
249
+ token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
250
+
251
+ const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
252
+ if (!str) return '';
253
+ const arr = splitTemplateVarStringRcs(str);
254
+ let varOrdinal = 0;
255
+ return arr
256
+ .map((elem) => {
257
+ if (rcsVarTestRegex.test(elem)) {
258
+ const key = getVarNameFromToken(elem);
259
+ const globalSlot = slotOffset + varOrdinal;
260
+ varOrdinal += 1;
261
+ const v = resolveCardVarMappedSlotValue(
262
+ cardVarMapped,
263
+ key,
264
+ globalSlot,
265
+ isLibraryMode,
266
+ );
267
+ if (v == null || String(v).trim() === '') return elem;
268
+ return String(v);
269
+ }
270
+ return elem;
271
+ })
272
+ .join('');
273
+ };
274
+
275
+ const effectiveTitle = textOnlyCard ? '' : String(title || '');
276
+ const titleVarCount = textOnlyCard
277
+ ? 0
278
+ : (effectiveTitle.match(rcsVarRegex) || []).length;
279
+ return {
280
+ rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle, 0),
281
+ rcsDesc: resolveTemplateWithMap(String(description || ''), titleVarCount),
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Campaign consumer payload: replace each card's `title` / `description` with VarSegment-resolved
287
+ * tag strings (same rules as {@link resolveRcsCardPreviewStrings}). Root `rcsCardVarMapped` merges
288
+ * with per-card `cardVarMapped`; `cardVarMapped` on each card is left unchanged for round-trip.
289
+ */
290
+ export function mapRcsCardContentForConsumerWithResolvedTags(
291
+ cardContentArray,
292
+ rootRcsCardVarMapped,
293
+ isFullMode,
294
+ ) {
295
+ const rootRecord =
296
+ rootRcsCardVarMapped != null && typeof rootRcsCardVarMapped === 'object'
297
+ ? rootRcsCardVarMapped
298
+ : {};
299
+ const list = Array.isArray(cardContentArray) ? cardContentArray : [];
300
+ const isLibraryMode = isFullMode !== true;
301
+ return list.map((card) => {
302
+ if (!card || typeof card !== 'object') return card;
303
+ const nested =
304
+ card.cardVarMapped != null && typeof card.cardVarMapped === 'object'
305
+ ? card.cardVarMapped
306
+ : {};
307
+ const merged = { ...rootRecord, ...nested };
308
+ const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
309
+ const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
310
+ card.title ?? '',
311
+ card.description ?? '',
312
+ merged,
313
+ isLibraryMode,
314
+ textOnly,
315
+ );
316
+ return {
317
+ ...card,
318
+ title: rcsTitle,
319
+ description: rcsDesc,
320
+ };
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Before save: strip only legacy numeric self-placeholders (`{{1}}`, `{{2}}`, …) mistakenly stored as
326
+ * slot values. Preserve semantic tokens like `{{FirstName}}` from TagList — those are valid mappings.
327
+ */
328
+ export function sanitizeCardVarMappedValue(val) {
329
+ if (val == null) return '';
330
+ const trimmedDisplayString = String(val).trim();
331
+ if (/^\{\{\d+\}\}$/.test(trimmedDisplayString)) return '';
332
+ return String(val);
333
+ }
334
+
335
+ /**
336
+ * Same completion rule as SmsTraiEdit RCS fallback — used by `isDisableDone` from
337
+ * `smsFallbackData.rcsSmsFallbackVarMapped` + template string.
338
+ * Every variable token (DLT {#…#} or mustache {{…}}) must have a non-empty trimmed value in the map.
339
+ *
340
+ * Slot keys are usually `${token}_${segmentIndex}` (same as VarSegmentMessageEditor). Persisted / API
341
+ * payloads may use `${token}_${varOrdinal}` with a 1-based occurrence index (see SmsTraiEdit init).
342
+ * We try segment index first, then ordinal — so e.g. template `{#var#}` (segment index 0) still matches
343
+ * map `{#var#}_1`.
344
+ *
345
+ * @param {string} templateText
346
+ * @param {Record<string, string>} [varSlotValueMap={}]
347
+ * @returns {boolean}
348
+ */
349
+ export function areAllRcsSmsFallbackVarSlotsFilled(templateText, varSlotValueMap = {}) {
350
+ if (!templateText || typeof templateText !== 'string') return true;
351
+ const segments = splitTemplateVarString(templateText, COMBINED_SMS_TEMPLATE_VAR_REGEX);
352
+ const hasVarToken = segments.some(
353
+ (segment) =>
354
+ typeof segment === 'string'
355
+ && isAnyTemplateVarToken(segment),
356
+ );
357
+ if (!hasVarToken) return true;
358
+ let varOrdinal = 0;
359
+ return segments.every((segment, segmentIndex) => {
360
+ if (
361
+ typeof segment !== 'string'
362
+ || !isAnyTemplateVarToken(segment)
363
+ ) return true;
364
+ varOrdinal += 1;
365
+ const indexKey = `${segment}_${segmentIndex}`;
366
+ const ordinalKey = `${segment}_${varOrdinal}`;
367
+ let mappedSlotValue;
368
+ if (Object.prototype.hasOwnProperty.call(varSlotValueMap, indexKey)) {
369
+ mappedSlotValue = varSlotValueMap[indexKey];
370
+ } else if (Object.prototype.hasOwnProperty.call(varSlotValueMap, ordinalKey)) {
371
+ mappedSlotValue = varSlotValueMap[ordinalKey];
372
+ } else {
373
+ mappedSlotValue = undefined;
374
+ }
375
+ if (mappedSlotValue == null) return false;
376
+ return String(mappedSlotValue).trim() !== '';
377
+ });
378
+ }
379
+
36
380
  export const getRCSContent = (template) => {
37
381
  const renderRcsSuggestionsPreview = (rcsSuggestions) => {
38
382
  const renderArray = [];
@@ -84,8 +428,10 @@ export const getRCSContent = (template) => {
84
428
  media = {},
85
429
  description,
86
430
  title,
431
+ mediaType,
87
432
  suggestions = [],
88
433
  } = cardContent[0];
434
+ const isTextOnlyCard = isRcsTextOnlyCardMediaType(mediaType);
89
435
  const mediaPreview = media?.thumbnailUrl ? media.thumbnailUrl : media.mediaUrl;
90
436
  return (
91
437
  <div className="cap-rcs-creatives">
@@ -95,13 +441,15 @@ export const getRCSContent = (template) => {
95
441
  className="rcs-listing-image"
96
442
  />
97
443
  )}
98
- <CapLabel
99
- type="label19"
100
- className="rcs-listing-content title"
101
- fontWeight="bold"
102
- >
103
- {title}
104
- </CapLabel>
444
+ {!isTextOnlyCard && (
445
+ <CapLabel
446
+ type="label19"
447
+ className="rcs-listing-content title"
448
+ fontWeight="bold"
449
+ >
450
+ {title}
451
+ </CapLabel>
452
+ )}
105
453
  <CapLabel type="label19" className="rcs-listing-content desc">
106
454
  {description}
107
455
  </CapLabel>
@@ -33,6 +33,10 @@ import injectReducer from '../../../utils/injectReducer';
33
33
  import v2SmsCreateReducer from './reducer';
34
34
  import * as globalActions from '../../Cap/actions';
35
35
  import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
36
+ import {
37
+ getSmsEmbeddedFooterValidity,
38
+ getSmsMessageFromFormData,
39
+ } from '../smsFormDataHelpers';
36
40
 
37
41
  export class Create extends React.Component { // eslint-disable-line react/prefer-stateless-function
38
42
  constructor(props) {
@@ -159,7 +163,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
159
163
  layout: 'SMS',
160
164
  type: 'TAG',
161
165
  context: this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default',
162
- embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
166
+ embedded: this.props.forceFullTagContext
167
+ ? 'full'
168
+ : (this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full'),
163
169
  };
164
170
  if (this.props.getDefaultTags) {
165
171
  query.context = this.props.getDefaultTags;
@@ -172,6 +178,16 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
172
178
  }
173
179
  }
174
180
 
181
+ componentDidUpdate() {
182
+ if (!this.props.embeddedSmsFallback || typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
183
+ return;
184
+ }
185
+ // Report validity on every update. The reducer in SmsFallback bails out (returns same
186
+ // state reference) when the value is unchanged, so no re-render loop is triggered.
187
+ // Calling unconditionally handles both mutation-based and reference-based FormBuilder updates.
188
+ this.props.onEmbeddedSmsFooterValidity(getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount));
189
+ }
190
+
175
191
  componentWillUnmount() {
176
192
  if (this.pendingGetFormDataTimeout) {
177
193
  clearTimeout(this.pendingGetFormDataTimeout);
@@ -275,6 +291,10 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
275
291
  }
276
292
  const result = {};
277
293
  result.base = baseData;
294
+ /* Root field used by FormBuilder; embedded getFormSubscriptionData reads value.base */
295
+ if (formData['template-name'] !== undefined) {
296
+ result.base['template-name'] = formData['template-name'];
297
+ }
278
298
  if (this.state.isValid) {
279
299
  const msgObj = charCount.updateCharCount(baseData['sms-editor']);
280
300
  result.base.msg_count = msgObj.msgCount;
@@ -881,7 +901,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
881
901
  layout: 'SMS',
882
902
  type: 'TAG',
883
903
  context: (data && data.toLowerCase() === 'all') ? 'default' : (data && data.toLowerCase()),
884
- embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
904
+ embedded: this.props.forceFullTagContext
905
+ ? 'full'
906
+ : (this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full'),
885
907
  };
886
908
  this.props.globalActions.fetchSchemaForEntity(query);
887
909
  }
@@ -889,10 +911,22 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
889
911
  removeStandAlone() {
890
912
  const schema = _.cloneDeep(this.state.schema);
891
913
  const childSections = _.get(schema, 'standalone.sections[0].childSections');
914
+ if (!childSections || childSections.length <= 2) {
915
+ return;
916
+ }
917
+ /* In-form Save / Test row removed for embedded SMS; slidebox footer (SlideBoxFooter) provides actions — see CreativesContainer. */
892
918
  childSections.splice(2, 1);
893
- const fields = _.get(childSections, '[1].childSections[0].childSections');//removing template name section
894
- fields.splice(0, 1);
895
- _.set(childSections, '[1].childSections[0].childSections', fields);
919
+ /*
920
+ * Creatives library also drops the standalone template-name block because `SlideBoxHeader` shows the name.
921
+ * RCS SMS fallback uses the same slidebox footer but no template-name header — keep Creative name in the form.
922
+ */
923
+ if (!this.props.embeddedSmsFallback) {
924
+ const fields = _.get(childSections, '[1].childSections[0].childSections');
925
+ if (fields && fields.length > 0) {
926
+ fields.splice(0, 1);
927
+ _.set(childSections, '[1].childSections[0].childSections', fields);
928
+ }
929
+ }
896
930
  _.set(schema, 'standalone.sections[0].childSections', childSections);
897
931
  this.setState({ schema, loadingStatus: this.state.loadingStatus + 1 });
898
932
  }
@@ -938,37 +972,8 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
938
972
  this.setState({startValidation: false});
939
973
  }
940
974
 
941
- getTemplateContent = () => {
942
- // Get SMS content from formData
943
- if (!this.state.formData || !Array.isArray(this.state.formData) || this.state.formData.length === 0) {
944
- return '';
945
- }
946
- const currentTabData = this.state.formData[this.state.currentTab - 1];
947
- if (!currentTabData) {
948
- return '';
949
- }
950
-
951
- // PRIORITY 1: Check direct path first (most common for SMS)
952
- // This handles: formData[0]['sms-editor']
953
- if (currentTabData['sms-editor']) {
954
- return currentTabData['sms-editor'];
955
- }
956
-
957
- // PRIORITY 2: Check activeTab structure (for versioned templates)
958
- // This handles: formData[0][activeTab]['sms-editor']
959
- const activeTab = currentTabData?.activeTab || 'base';
960
- if (currentTabData[activeTab]?.['sms-editor']) {
961
- return currentTabData[activeTab]['sms-editor'];
962
- }
963
-
964
- // PRIORITY 3: Check base explicitly (fallback)
965
- // This handles: formData[0]['base']['sms-editor']
966
- if (currentTabData['base']?.['sms-editor']) {
967
- return currentTabData['base']['sms-editor'];
968
- }
969
-
970
- return '';
971
- }
975
+ getTemplateContent = () =>
976
+ getSmsMessageFromFormData(this.state.formData, this.state.currentTab);
972
977
 
973
978
  handleTestAndPreview = () => {
974
979
  // If parent is managing state (props exist), call parent handler
@@ -997,6 +1002,35 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
997
1002
  }
998
1003
 
999
1004
  saveFormData() {
1005
+ /*
1006
+ * RCS SMS fallback slidebox (embeddedSmsFallback): parent passes isFullMode from RCS, but we must not
1007
+ * call createTemplate — that spins CapSpin on createTemplateInProgress and is not the embedded contract.
1008
+ * Same as library: hand off form payload via getFormSubscriptionData.
1009
+ */
1010
+ if (this.props.embeddedSmsFallback && this.props.getFormSubscriptionData) {
1011
+ const { isTemplateNameEmpty, isMessageEmpty } = getSmsEmbeddedFooterValidity(
1012
+ this.state.formData,
1013
+ this.state.tabCount,
1014
+ );
1015
+ if (isTemplateNameEmpty || isMessageEmpty) {
1016
+ this.setState({ startValidation: true, pendingGetFormData: false });
1017
+ if (this.props.onValidationFail) {
1018
+ this.props.onValidationFail();
1019
+ }
1020
+ return;
1021
+ }
1022
+ const payload = this.getFormData();
1023
+ if (!payload.validity) {
1024
+ if (this.props.onValidationFail) {
1025
+ this.props.onValidationFail();
1026
+ }
1027
+ this.setState({ pendingGetFormData: false, startValidation: false });
1028
+ return;
1029
+ }
1030
+ this.props.getFormSubscriptionData(payload);
1031
+ this.setState({ pendingGetFormData: false, startValidation: false });
1032
+ return;
1033
+ }
1000
1034
  // In library mode: FormBuilder calls onSubmit only after liquid validation succeeds.
1001
1035
  // Submit to parent here so the slidebox can close with valid data.
1002
1036
  if (!this.props.isFullMode) {
@@ -1096,6 +1130,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
1096
1130
  onTestContentClicked={this.props.onTestContentClicked}
1097
1131
  onPreviewContentClicked={this.props.onPreviewContentClicked}
1098
1132
  eventContextTags={this.props?.eventContextTags}
1133
+ tagListGetPopupContainer={this.props.tagListGetPopupContainer}
1134
+ tagListPopoverOverlayStyle={this.props.tagListPopoverOverlayStyle}
1135
+ tagListPopoverOverlayClassName={this.props.tagListPopoverOverlayClassName}
1099
1136
  />
1100
1137
  </CapColumn>
1101
1138
  </CapRow>
@@ -1108,6 +1145,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
1108
1145
  formData={this.state.formData}
1109
1146
  content={this.getTemplateContent()}
1110
1147
  currentChannel={SMS}
1148
+ smsRegister={this.props.smsRegister}
1111
1149
  />
1112
1150
  </div>
1113
1151
  );
@@ -1136,6 +1174,13 @@ Create.propTypes = {
1136
1174
  handleTestAndPreview: PropTypes.func,
1137
1175
  handleCloseTestAndPreview: PropTypes.func,
1138
1176
  isTestAndPreviewMode: PropTypes.bool,
1177
+ smsRegister: PropTypes.any,
1178
+ forceFullTagContext: PropTypes.bool,
1179
+ embeddedSmsFallback: PropTypes.bool,
1180
+ onEmbeddedSmsFooterValidity: PropTypes.func,
1181
+ tagListGetPopupContainer: PropTypes.func,
1182
+ tagListPopoverOverlayStyle: PropTypes.object,
1183
+ tagListPopoverOverlayClassName: PropTypes.string,
1139
1184
  };
1140
1185
 
1141
1186
  const mapStateToProps = createStructuredSelector({
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared SMS FormBuilder formData helpers for Sms/Create (and any embedded host).
3
+ */
4
+
5
+ /**
6
+ * @param {object} formData FormBuilder state (same shape as Sms/Create `this.state.formData`)
7
+ * @param {number} currentTab 1-based tab index
8
+ * @returns {string} Raw message body or ''
9
+ */
10
+ export function getSmsMessageFromFormData(formData, currentTab) {
11
+ if (formData == null || typeof formData !== 'object') {
12
+ return '';
13
+ }
14
+ const tab = currentTab != null && currentTab > 0 ? currentTab : 1;
15
+ const currentTabData = formData[tab - 1];
16
+ if (currentTabData && typeof currentTabData === 'object') {
17
+ const versionedKey = tab > 1 ? `sms-editor${tab}` : 'sms-editor';
18
+ if (Object.prototype.hasOwnProperty.call(currentTabData, versionedKey)) {
19
+ const v = currentTabData[versionedKey];
20
+ // Key exists — commit to this version's value rather than falling through to base.
21
+ // Treat null/undefined as empty so a cleared version correctly reports as empty.
22
+ return (v != null && v !== '') ? String(v) : '';
23
+ }
24
+ if (currentTabData['sms-editor'] != null) {
25
+ return String(currentTabData['sms-editor']);
26
+ }
27
+ const activeTab = currentTabData.activeTab || 'base';
28
+ if (currentTabData[activeTab]?.['sms-editor'] != null) {
29
+ return String(currentTabData[activeTab]['sms-editor']);
30
+ }
31
+ if (currentTabData.base?.['sms-editor'] != null) {
32
+ return String(currentTabData.base['sms-editor']);
33
+ }
34
+ }
35
+ const rootBase = formData.base;
36
+ if (rootBase && typeof rootBase === 'object' && rootBase['sms-editor'] != null) {
37
+ return String(rootBase['sms-editor']);
38
+ }
39
+ return '';
40
+ }
41
+
42
+ /**
43
+ * @param {number} [tabCount] Total number of versions/tabs. When >1 all versions are checked.
44
+ * @returns {{ isTemplateNameEmpty: boolean, isMessageEmpty: boolean }}
45
+ */
46
+ export function getSmsEmbeddedFooterValidity(formData, tabCount) {
47
+ const rawName = formData?.['template-name'];
48
+ const name = rawName != null && rawName !== '' ? String(rawName).trim() : '';
49
+
50
+ // Check ALL versions: if any version's message is empty, Done should be disabled.
51
+ // With a single version this is equivalent to the previous single-tab check.
52
+ const count = tabCount != null && tabCount > 1 ? tabCount : 1;
53
+ let isMessageEmpty = false;
54
+ for (let i = 1; i <= count; i++) {
55
+ const content = getSmsMessageFromFormData(formData, i);
56
+ const msg = content != null && content !== '' ? String(content).trim() : '';
57
+ if (!msg) {
58
+ isMessageEmpty = true;
59
+ break;
60
+ }
61
+ }
62
+
63
+ return {
64
+ isTemplateNameEmpty: !name,
65
+ isMessageEmpty,
66
+ };
67
+ }