@capillarytech/creatives-library 9.0.35-alpha.0 → 9.0.35

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.
@@ -71,6 +71,7 @@ import {
71
71
  mergeSmsFallbackForLibrary,
72
72
  buildSmsFallBackContentForPayload,
73
73
  pickRcsCardVarMappedEntries,
74
+ extractRegisteredSenderIdsFromSmsFallbackRecord,
74
75
  } from './rcsLibraryHydrationUtils';
75
76
  import {
76
77
  RCS,
@@ -152,7 +153,6 @@ import { isTagIncluded } from '../../utils/commonUtils';
152
153
  import injectReducer from '../../utils/injectReducer';
153
154
  import v2RcsReducer from './reducer';
154
155
  import {
155
- buildRcsNumericMustachePlaceholderRegex,
156
156
  getTemplateStatusType,
157
157
  normalizeCardVarMapped,
158
158
  coalesceCardVarMappedToTemplate,
@@ -175,6 +175,7 @@ import {
175
175
  getCarouselDescriptionCharacterCount,
176
176
  buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
177
177
  buildCarouselCardContentForPayload,
178
+ getCarouselVarMapKey,
178
179
  } from './carouselUtils';
179
180
  import CarouselDimensionSelection from './components/CarouselDimensionSelection';
180
181
  import CarouselCard from './components/CarouselCard';
@@ -775,7 +776,7 @@ export const Rcs = (props) => {
775
776
  ['title', 'description'].forEach((field) => {
776
777
  const templateStr = card?.[field] || '';
777
778
  if (!templateStr) return;
778
- const resolved = resolveTemplateWithMap(templateStr);
779
+ const resolved = resolveCarouselTemplateWithMap(templateStr, idx);
779
780
  if (!resolved) {
780
781
  updateCarouselErrors(idx, { [field]: false });
781
782
  return;
@@ -867,8 +868,27 @@ export const Rcs = (props) => {
867
868
  }).join('');
868
869
  };
869
870
 
871
+ /**
872
+ * Carousel resolve: each card's variables live under their own `getCarouselVarMapKey(cardIndex, name)`
873
+ * key in `cardVarMapped` (see that helper's doc) — no global slot-offset arithmetic needed since
874
+ * cards can no longer collide with each other's entries.
875
+ */
876
+ const resolveCarouselTemplateWithMap = (str = '', cardIndex) => {
877
+ if (!str) return '';
878
+ const arr = splitTemplateVarStringRcs(str);
879
+ return arr.map((elem) => {
880
+ if (rcsVarTestRegex.test(elem)) {
881
+ const varName = getVarNameFromToken(elem);
882
+ const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, varName)];
883
+ if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
884
+ return String(slotValue);
885
+ }
886
+ return elem;
887
+ }).join('');
888
+ };
889
+
870
890
  const buildCarouselCardsForPreview = (cards = []) =>
871
- buildCarouselCardsForPreviewUtil(cards, { isFullMode, rcsVarRegex, resolveTemplateWithMap });
891
+ buildCarouselCardsForPreviewUtil(cards, { isFullMode, resolveCarouselTemplateWithMap });
872
892
 
873
893
  /**
874
894
  * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
@@ -1196,28 +1216,56 @@ export const Rcs = (props) => {
1196
1216
 
1197
1217
  setTemplateType(contentType.carousel);
1198
1218
  setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1199
- // Library/campaign payloads can store cardSettings/cardContent at the flat top-level
1200
- // `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
1201
- // used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
1202
- // silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
1203
- // actually uploaded at and causing object-fit:cover to crop the preview image.
1219
+
1204
1220
  const cardSettings = rcsContent?.cardSettings
1205
1221
  || get(details, 'rcsContent.cardSettings', {});
1206
1222
  const cardWidth = cardSettings?.cardWidth || SMALL;
1207
1223
  setSelectedCarouselWidth(cardWidth);
1208
1224
 
1209
- const cards = Array.isArray(rcsContent?.cardContent)
1210
- ? rcsContent.cardContent
1211
- : get(details, 'rcsContent.cardContent', []);
1225
+ const nestedCards = Array.isArray(rcsContent?.cardContent) ? rcsContent.cardContent : [];
1226
+ const topCardsRaw = get(details, 'rcsContent.cardContent', []);
1227
+ const topCards = Array.isArray(topCardsRaw) ? topCardsRaw : [];
1228
+ const cardCount = Math.max(nestedCards.length, topCards.length);
1229
+ const mergedCards = Array.from({ length: cardCount }, (_, idx) => ({
1230
+ ...(topCards[idx] || {}),
1231
+ ...(nestedCards[idx] || {}),
1232
+ }));
1233
+ const cards = cardCount > 0 ? mergedCards : topCards;
1212
1234
  const firstHeight = cards?.[0]?.media?.height || MEDIUM;
1213
1235
  setSelectedCarouselHeight(firstHeight);
1214
1236
  setSelectedCarousel(`${firstHeight}_${cardWidth}`);
1215
1237
  setActiveCarouselIndex('0');
1216
1238
 
1217
1239
  if (cards.length > 1) {
1218
- setCardVarMapped((previousVarMap) => cards.reduce((merged, c) => {
1219
- const cardMap = c?.cardVarMapped;
1220
- return cardMap != null && typeof cardMap === 'object' ? { ...merged, ...cardMap } : merged;
1240
+
1241
+ setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}, idx) => {
1242
+ const rawCardMap = c?.cardVarMapped;
1243
+ if (rawCardMap == null || typeof rawCardMap !== 'object') return merged;
1244
+ if (idx === 0) return { ...merged, ...rawCardMap };
1245
+ const cardTitle = c?.title != null ? String(c.title) : '';
1246
+ const cardDesc = c?.description != null ? String(c.description) : '';
1247
+ const cardTokens = [
1248
+ ...(cardTitle ? cardTitle.match(rcsVarRegex) ?? [] : []),
1249
+ ...(cardDesc ? cardDesc.match(rcsVarRegex) ?? [] : []),
1250
+ ];
1251
+ const cardOrderedTagNames = cardTokens.map((token) => getVarNameFromToken(token)).filter(Boolean);
1252
+ const cardMapBeforeCoalesce = isFullMode
1253
+ ? normalizeCardVarMapped(rawCardMap, cardOrderedTagNames)
1254
+ : { ...rawCardMap };
1255
+ const cardMapAfterCoalesce = coalesceCardVarMappedToTemplate(
1256
+ cardMapBeforeCoalesce,
1257
+ cardTitle,
1258
+ cardDesc,
1259
+ rcsVarRegex,
1260
+ );
1261
+ const cardMapAfterSlotSync = !isFullMode
1262
+ ? syncCardVarMappedSemanticsFromSlots(cardMapAfterCoalesce, cardTitle, cardDesc, rcsVarRegex)
1263
+ : cardMapAfterCoalesce;
1264
+ const namespacedCardMap = {};
1265
+ Object.entries(cardMapAfterSlotSync).forEach(([key, value]) => {
1266
+ namespacedCardMap[getCarouselVarMapKey(idx, key)] = value;
1267
+ });
1268
+ return { ...merged, ...namespacedCardMap };
1221
1269
  }, { ...(previousVarMap || {}) }));
1222
1270
  }
1223
1271
 
@@ -1426,12 +1474,6 @@ export const Rcs = (props) => {
1426
1474
  globalActions.fetchSchemaForEntity(query);
1427
1475
  };
1428
1476
 
1429
- const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
1430
- if (!templateStr || !numericVarName || !tagName) return templateStr;
1431
- const placeholderRegex = buildRcsNumericMustachePlaceholderRegex(numericVarName);
1432
- return templateStr.replace(placeholderRegex, `{{${tagName}}}`);
1433
- };
1434
-
1435
1477
  /**
1436
1478
  * Replaces only the var token at `slotOrdinalZeroBased` (the field-local ordinal among
1437
1479
  * `{{...}}` tokens, left to right) with `{{tagName}}`. Unlike a name-based regex replace,
@@ -1484,16 +1526,15 @@ export const Rcs = (props) => {
1484
1526
  setCardVarMapped((previousCardVarMapped) => {
1485
1527
  const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
1486
1528
  if (isNumericPlaceholderSlot) {
1529
+ // Numeric slot ({{1}}, {{2}}, …) is the persisted API contract — keep the numeric key
1530
+ // itself (do not fork off a semantic-name key); the template text stays {{N}} below.
1487
1531
  const existingValueBeforeAppend = (
1488
1532
  previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
1489
1533
  ).toString();
1490
1534
  const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1491
- delete updatedCardVarMapped[semanticOrNumericVarName];
1492
- updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
1535
+ updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
1493
1536
  } else {
1494
- // Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
1495
- // "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
1496
- // read/write the global numeric slot only and drop the shared semantic key.
1537
+
1497
1538
  const existingValueBeforeAppend = cardVarMappedNumericSlotKey
1498
1539
  ? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
1499
1540
  : String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
@@ -1522,10 +1563,15 @@ export const Rcs = (props) => {
1522
1563
  return updatedCardVarMapped;
1523
1564
  });
1524
1565
 
1566
+ // Numeric slot ({{1}}, {{2}}, …): the token is already the persisted API contract — leave
1567
+ // templateTitle/templateDesc untouched. The chip label re-resolves via titleVarSegmentValueMapById
1568
+ // / descriptionVarSegmentValueMapById, both keyed off cardVarMapped (updated above).
1569
+ if (isNumericPlaceholderSlot) return;
1570
+
1525
1571
  if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC) {
1526
- // A numeric slot (e.g. {{1}}) is replaced by its unique placeholder text; a slot that
1527
- // already holds a named tag (e.g. {{first_name}}) is replaced by field-local position,
1528
- // since a name-based replace would also overwrite other slots sharing that same tag name.
1572
+ // Legacy slot that already holds a named tag (e.g. {{first_name}}) is replaced by
1573
+ // field-local position, since a name-based replace would also overwrite other slots
1574
+ // sharing that same tag name.
1529
1575
  const fieldOffsetForSlot = tagAreaField === RCS_TAG_AREA_FIELD_TITLE
1530
1576
  ? 0
1531
1577
  : (templateTitle?.match(rcsVarRegex) ?? []).length;
@@ -1535,32 +1581,20 @@ export const Rcs = (props) => {
1535
1581
 
1536
1582
  if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
1537
1583
  setTemplateTitle((previousTitle) => {
1538
- const titleAfterReplacingTag = isNumericPlaceholderSlot
1539
- ? replaceNumericPlaceholderWithTagInTemplate(
1540
- previousTitle || '',
1541
- semanticOrNumericVarName,
1542
- selectedTagNameFromPicker,
1543
- )
1544
- : replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal, selectedTagNameFromPicker);
1584
+ const titleAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal, selectedTagNameFromPicker);
1545
1585
  if (titleAfterReplacingTag === previousTitle) return previousTitle;
1546
- setTemplateTitleError(variableErrorHandling(titleAfterReplacingTag));
1586
+ setTemplateTitleError(computeTemplateTitleError(titleAfterReplacingTag));
1547
1587
  // Remount segment editor: tag insert replaces {{n}} with e.g. {{tag.FORMAT_1}} — slot ids change; avoids stale UI vs manual typing in full-mode TextArea
1548
1588
  setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
1549
1589
  return titleAfterReplacingTag;
1550
1590
  });
1551
1591
  } else {
1552
1592
  setTemplateDesc((previousDescription) => {
1553
- const descriptionAfterReplacingTag = isNumericPlaceholderSlot
1554
- ? replaceNumericPlaceholderWithTagInTemplate(
1555
- previousDescription || '',
1556
- semanticOrNumericVarName,
1557
- selectedTagNameFromPicker,
1558
- )
1559
- : replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal, selectedTagNameFromPicker);
1593
+ const descriptionAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal, selectedTagNameFromPicker);
1560
1594
  if (descriptionAfterReplacingTag === previousDescription) {
1561
1595
  return previousDescription;
1562
1596
  }
1563
- setTemplateDescError(variableErrorHandling(descriptionAfterReplacingTag));
1597
+ setTemplateDescError(computeTemplateDescError(descriptionAfterReplacingTag));
1564
1598
  setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
1565
1599
  return descriptionAfterReplacingTag;
1566
1600
  });
@@ -1579,12 +1613,14 @@ export const Rcs = (props) => {
1579
1613
  const token = carouselFocusedVarId.slice(0, sep);
1580
1614
  const variableName = getVarNameFromToken(token);
1581
1615
  if (!variableName) return;
1616
+ const activeIdx = parseInt(activeCarouselIndex, 10);
1617
+ const mapKey = getCarouselVarMapKey(isNaN(activeIdx) ? 0 : activeIdx, variableName);
1582
1618
  setCardVarMapped((prev) => {
1583
- const base = (prev?.[variableName] ?? '').toString();
1619
+ const base = (prev?.[mapKey] ?? '').toString();
1584
1620
  const nextVal = `${base}{{${data}}}`;
1585
1621
  return {
1586
1622
  ...(prev || {}),
1587
- [variableName]: nextVal,
1623
+ [mapKey]: nextVal,
1588
1624
  };
1589
1625
  });
1590
1626
  };
@@ -1696,31 +1732,13 @@ export const Rcs = (props) => {
1696
1732
  return renderArray;
1697
1733
  };
1698
1734
  const onTemplateTitleChange = ({ target: { value } }) => {
1699
- let errorMessage = false;
1700
- if (templateType === contentType.rich_card && !value.trim()) {
1701
- errorMessage = formatMessage(messages.emptyTemplateTitleErrorMessage);
1702
- } else if (value.length > TEMPLATE_TITLE_MAX_LENGTH) {
1703
- errorMessage = formatMessage(messages.templateHeaderLengthError);
1704
- } else {
1705
- errorMessage = variableErrorHandling(value);
1706
- }
1707
1735
  setTemplateTitle(value);
1708
- setTemplateTitleError(errorMessage);
1736
+ setTemplateTitleError(computeTemplateTitleError(value));
1709
1737
  };
1710
1738
 
1711
1739
  const onTemplateDescChange = ({ target: { value } }) => {
1712
- let errorMessage = false;
1713
- if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
1714
- errorMessage = formatMessage(messages.templateMessageLengthError);
1715
- } else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
1716
- errorMessage = formatMessage(messages.templateMessageLengthError);
1717
- } else {
1718
- errorMessage = false;
1719
- }
1720
- const varError = variableErrorHandling(value);
1721
- const error = errorMessage || varError;
1722
1740
  setTemplateDesc(value);
1723
- setTemplateDescError(error);
1741
+ setTemplateDescError(computeTemplateDescError(value));
1724
1742
  };
1725
1743
 
1726
1744
 
@@ -1803,6 +1821,29 @@ export const Rcs = (props) => {
1803
1821
  return false;
1804
1822
  };
1805
1823
 
1824
+ // Shared with onTemplateTitleChange so tag-picker inserts enforce the same length limit as manual typing.
1825
+ const computeTemplateTitleError = (value) => {
1826
+ if (templateType === contentType.rich_card && !value.trim()) {
1827
+ return formatMessage(messages.emptyTemplateTitleErrorMessage);
1828
+ }
1829
+ if (value.length > TEMPLATE_TITLE_MAX_LENGTH) {
1830
+ return formatMessage(messages.templateHeaderLengthError);
1831
+ }
1832
+ return variableErrorHandling(value);
1833
+ };
1834
+
1835
+ // Shared with onTemplateDescChange so tag-picker inserts enforce the same length limit as manual typing.
1836
+ const computeTemplateDescError = (value) => {
1837
+ let errorMessage = false;
1838
+ if (templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)) {
1839
+ errorMessage = formatMessage(messages.templateMessageLengthError);
1840
+ } else if (templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH) {
1841
+ errorMessage = formatMessage(messages.templateMessageLengthError);
1842
+ }
1843
+ const varError = variableErrorHandling(value);
1844
+ return errorMessage || varError;
1845
+ };
1846
+
1806
1847
  const onMessageAddVar = () => {
1807
1848
  onAddVar(templateDesc);
1808
1849
  };
@@ -1887,23 +1928,30 @@ const onTitleAddVar = () => {
1887
1928
  return "";
1888
1929
  };
1889
1930
 
1890
- // Carousel: render variable-value editor for a given template string (title/description).
1891
- // This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
1892
- const renderCarouselEditMessage = (templateStr) => {
1931
+ // Carousel: render variable-value editor for a given template string (title/description) belonging
1932
+ // to `cardIndex`. This matches rich-card/text edit behavior: static pieces are read-only, variable
1933
+ // tokens are editable. Each card's values live under their own `getCarouselVarMapKey(cardIndex, name)`
1934
+ // key in `cardVarMapped` so two cards using the same variable/tag name never collide.
1935
+ const renderCarouselEditMessage = (templateStr, cardIndex) => {
1893
1936
  const renderArray = [];
1894
1937
  const templateArr = splitTemplateVarString(templateStr);
1895
1938
  if (templateArr?.length) {
1896
1939
  templateArr.forEach((elem, index) => {
1897
1940
  if (rcsVarTestRegex.test(elem)) {
1898
1941
  const varName = getVarNameFromToken(elem);
1942
+ const mapKey = getCarouselVarMapKey(cardIndex, varName);
1943
+ const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
1899
1944
  renderArray.push(
1900
- <div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
1945
+ <div
1946
+ key={`${elem}_${index}_${rcsVarSegmentEditorRemountKey}`}
1947
+ className="var-segment-message-editor__var-slot"
1948
+ >
1901
1949
  <TextArea
1902
1950
  id={`${elem}_${index}`}
1903
1951
  placeholder={`enter the value for ${elem}`}
1904
1952
  autosize={{ minRows: 1, maxRows: 3 }}
1905
- onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
1906
- value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
1953
+ onChange={(e) => textAreaValueChange(e, cardIndex)}
1954
+ value={computedValue}
1907
1955
  onFocus={(e) => {
1908
1956
  const id = e?.target?.id || e?.currentTarget?.id || '';
1909
1957
  setCarouselFocusedVarId(id);
@@ -1927,7 +1975,7 @@ const onTitleAddVar = () => {
1927
1975
  return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
1928
1976
  };
1929
1977
 
1930
- const textAreaValueChange = (e, type) => {
1978
+ const textAreaValueChange = (e, cardIndex) => {
1931
1979
  const value = e?.target?.value ?? '';
1932
1980
  const id = e?.target?.id || e?.currentTarget?.id || '';
1933
1981
  if (!id) return;
@@ -1938,9 +1986,10 @@ const onTitleAddVar = () => {
1938
1986
  const variableName = getVarNameFromToken(token);
1939
1987
 
1940
1988
  if (variableName) {
1989
+ const mapKey = getCarouselVarMapKey(cardIndex, variableName);
1941
1990
  setCardVarMapped((prev) => ({
1942
1991
  ...prev,
1943
- [variableName]: isInvalidValue ? "" : value,
1992
+ [mapKey]: isInvalidValue ? "" : value,
1944
1993
  }));
1945
1994
  }
1946
1995
  };
@@ -2608,8 +2657,6 @@ const onTitleAddVar = () => {
2608
2657
  const carouselVidDims =
2609
2658
  RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
2610
2659
  || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
2611
- // Debug log for embedded/library mode preview payload (carousel)
2612
- // eslint-disable-next-line no-console
2613
2660
  return (
2614
2661
  <UnifiedPreview
2615
2662
  channel={RCS}
@@ -2793,46 +2840,55 @@ const onTitleAddVar = () => {
2793
2840
  const cardContent = rcsForTest.rcsContent?.cardContent;
2794
2841
  if (Array.isArray(cardContent) && cardContent[0]) {
2795
2842
  if (isCarouselType) {
2796
- // Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
2797
- // user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
2843
+ // Carousel: resolve {{N}}/named slot tokens to the actual tag expressions / static values
2844
+ // the user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
2798
2845
  // - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
2799
2846
  // - prepareTagExtractionPayload can extract tag metadata from the card content.
2800
- // Track cumulative slotOffset across cards: each card's title/description vars occupy
2801
- // sequential global slot indices ({{1}},{{2}} in card 0; {{3}},{{4}} in card 1, …).
2802
- // Without the offset, every card restarts at slotKey="1" and resolves against card 0's
2803
- // mappings, causing tags from card 1+ to be missing or wrong.
2804
- let carouselSlotOffset = 0;
2847
+ // Each card's variables live under their own getCarouselVarMapKey(cardIndex, name) slot, so
2848
+ // no cross-card slot-offset bookkeeping is needed (or safe two cards' variables never share
2849
+ // a key, so there is nothing to accidentally collide).
2805
2850
  rcsForTest = {
2806
2851
  ...rcsForTest,
2807
2852
  rcsContent: {
2808
2853
  ...rcsForTest.rcsContent,
2809
- cardContent: cardContent.map((card) => {
2854
+ cardContent: cardContent.map((card, cardIndex) => {
2810
2855
  const rawTitle = card.title || '';
2811
2856
  const rawDesc = card.description || '';
2812
- const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
2813
- const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
2814
- const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
2815
- const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
2816
- carouselSlotOffset += titleVarCount + descVarCount;
2857
+ const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex);
2858
+ const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex);
2817
2859
  return { ...card, title: resolvedTitle, description: resolvedDesc };
2818
2860
  }),
2819
2861
  },
2820
2862
  };
2821
2863
  } else if (isSlotMappingModeForPreview) {
2822
2864
  // Standalone card: coalesce cardVarMapped with the template's slot names so the preview
2823
- // API receives a correctly-keyed var map for non-carousel templates.
2865
+ // API receives a correctly-keyed var map for non-carousel templates. Also resolve the
2866
+ // {{N}} slot tokens in title/description themselves (mirrors the carousel branch above) —
2867
+ // otherwise prepareTagExtractionPayload/getRcsPrimaryTagExtractionText send the raw "{{1}}"
2868
+ // token as template text, which the extract-tags API doesn't recognize, so the
2869
+ // synthetic-tag fallback names the row "1" instead of the real tag.
2824
2870
  const fullCardVarMapped = coalesceCardVarMappedToTemplate(
2825
2871
  pickRcsCardVarMappedEntries(cardVarMapped),
2826
2872
  templateTitle,
2827
2873
  templateDesc,
2828
2874
  rcsVarRegex,
2829
2875
  );
2876
+ const rawStandaloneTitle = cardContent[0].title || '';
2877
+ const rawStandaloneDesc = cardContent[0].description || '';
2878
+ const standaloneTitleVarCount = (rawStandaloneTitle.match(rcsVarRegex) || []).length;
2879
+ const resolvedStandaloneTitle = resolveTemplateWithMap(rawStandaloneTitle, 0);
2880
+ const resolvedStandaloneDesc = resolveTemplateWithMap(rawStandaloneDesc, standaloneTitleVarCount);
2830
2881
  rcsForTest = {
2831
2882
  ...rcsForTest,
2832
2883
  rcsContent: {
2833
2884
  ...rcsForTest.rcsContent,
2834
2885
  cardContent: [
2835
- { ...cardContent[0], cardVarMapped: fullCardVarMapped },
2886
+ {
2887
+ ...cardContent[0],
2888
+ title: resolvedStandaloneTitle,
2889
+ description: resolvedStandaloneDesc,
2890
+ cardVarMapped: fullCardVarMapped,
2891
+ },
2836
2892
  ...cardContent.slice(1),
2837
2893
  ],
2838
2894
  },
@@ -2848,16 +2904,23 @@ const onTitleAddVar = () => {
2848
2904
  },
2849
2905
  },
2850
2906
  };
2851
- // Use the smsFallbackTemplateConfigs already built by createPayload() / buildSmsFallBackContentForPayload —
2852
- // it merges the API baseline (templateId, registeredSenderIds, templateName, etc.) with local
2853
- // smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced smsTemplateId
2854
- // in campaign mode. This also keeps templateName (and all other fields) in sync with the approval
2855
- // payload automatically.
2856
- const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
2857
- if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
2907
+
2908
+ // buildSmsFallBackContentForPayload nests registeredSenderIds under smsFallBackContent.templateConfigs
2909
+ // only for full mode, or for campaign mode when isDltCampaign (smsRegister === 'DLT') is true — but it
2910
+ // always sets the flat smsFallBackContent.registeredSenderIds whenever the fallback template has them.
2911
+ // Extract via both so Test & Preview's DLT sender-id filter still sees real ids (and actually filters)
2912
+ // even when isDltCampaign was false at payload-build time.
2913
+ const smsFallbackRecordForTest = rcs?.rcsContent?.smsFallBackContent;
2914
+ const smsFallbackTcFromPayload = smsFallbackRecordForTest?.templateConfigs;
2915
+ const registeredSenderIdsForTest = extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackRecordForTest);
2916
+ const hasRegisteredSenderIdsForTest =
2917
+ Array.isArray(registeredSenderIdsForTest) && registeredSenderIdsForTest.length > 0;
2918
+ if ((smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') || hasRegisteredSenderIdsForTest) {
2858
2919
  out.templateConfigs = {
2859
- ...smsFallbackTcFromPayload,
2860
- traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
2920
+ ...(smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object' ? smsFallbackTcFromPayload : {}),
2921
+ ...(hasRegisteredSenderIdsForTest && { registeredSenderIds: registeredSenderIdsForTest }),
2922
+ // Real registered sender IDs mean filtering should happen regardless of the smsRegister heuristic.
2923
+ traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister) || hasRegisteredSenderIdsForTest,
2861
2924
  };
2862
2925
  }
2863
2926
  return out;
@@ -3003,12 +3066,12 @@ const onTitleAddVar = () => {
3003
3066
  ['title', 'description'].some((field) => !(card?.[field] || '').trim())
3004
3067
  );
3005
3068
  if (hasEmptyField) return true;
3006
- const unfilledVar = (carouselData || []).some((card) =>
3069
+ const unfilledVar = (carouselData || []).some((card, cardIndex) =>
3007
3070
  ['title', 'description'].some((field) => {
3008
3071
  const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
3009
3072
  return tokens.some((token) => {
3010
3073
  const name = token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
3011
- const slotValue = cardVarMapped?.[name];
3074
+ const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, name)];
3012
3075
  return slotValue == null || String(slotValue).trim() === '';
3013
3076
  });
3014
3077
  })
@@ -217,6 +217,24 @@ export function buildSmsFallBackContentForPayload({
217
217
  ? tcSibling.header
218
218
  : null;
219
219
  const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
220
+ // `rcsSmsFallbackVarMapped` (root of smsFallBackContent) is a custom field that
221
+ // normalizeRcsMessageContentForApi strips before the real API call (see comment below) — it
222
+ // only survives for same-session local re-hydration. templateConfigs.templateVariableMapping is
223
+ // the field the backend actually persists (same shape TRAI/DLT registration uses), so mirror the
224
+ // var map there too — otherwise slot values saved through campaigns/library mode are lost on reopen.
225
+ const varMapForTemplateConfigs =
226
+ smsFallbackForPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
227
+ || mergedFallback[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
228
+ const templateVariableMappingForPayload =
229
+ varMapForTemplateConfigs && typeof varMapForTemplateConfigs === 'object'
230
+ ? Object.entries(varMapForTemplateConfigs).reduce((acc, [slotKey, slotValue], index) => {
231
+ if (slotValue == null || slotValue === '') return acc;
232
+ acc[slotKey] = { data: slotValue, count: index + 1 };
233
+ return acc;
234
+ }, {})
235
+ : null;
236
+ const hasTemplateVariableMapping =
237
+ templateVariableMappingForPayload && Object.keys(templateVariableMappingForPayload).length > 0;
220
238
  const smsFallbackTemplateConfigs =
221
239
  smsFallbackTemplateId || hasRegisteredSenderIds
222
240
  ? {
@@ -228,6 +246,9 @@ export function buildSmsFallBackContentForPayload({
228
246
  ...(hasRegisteredSenderIds && {
229
247
  registeredSenderIds: registeredSenderIdsForPayload,
230
248
  }),
249
+ ...(hasTemplateVariableMapping && {
250
+ templateVariableMapping: templateVariableMappingForPayload,
251
+ }),
231
252
  }
232
253
  : null;
233
254
  const isDltCampaign = !isFullMode && isTraiDLTEnable(isFullMode, smsRegister);
@@ -295,17 +316,37 @@ export function resolveSmsFallbackHydrationFromDetails(details) {
295
316
  const fromNested = Array.isArray(updatedEditor)
296
317
  ? updatedEditor.join('')
297
318
  : (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
298
- const fallbackMessage = smsFallbackContent.smsContent
319
+ const templateConfigsRecord =
320
+ smsFallbackContent.templateConfigs && typeof smsFallbackContent.templateConfigs === 'object'
321
+ ? smsFallbackContent.templateConfigs
322
+ : {};
323
+ // For DLT/TRAI fallbacks, smsContent/smsTemplateContent/message are the send-ready text —
324
+ // {#var#} slots are already baked into a literal value there (required to match the
325
+ // TRAI-approved template). Only templateConfigs.template keeps {#var#} intact, so prefer it
326
+ // here — otherwise reopening the editor can never re-render that slot as its own box again.
327
+ const rawDltTemplate =
328
+ typeof templateConfigsRecord.template === 'string' && templateConfigsRecord.template.trim() !== ''
329
+ ? templateConfigsRecord.template
330
+ : '';
331
+ const fallbackMessage = rawDltTemplate
332
+ || smsFallbackContent.smsContent
299
333
  || smsFallbackContent.smsTemplateContent
300
334
  || smsFallbackContent.message
301
335
  || fromNested
302
336
  || '';
303
- const varMappedFromPayload = smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
337
+ const varMapFromTemplateVariableMapping =
338
+ templateConfigsRecord.templateVariableMapping && typeof templateConfigsRecord.templateVariableMapping === 'object'
339
+ ? Object.entries(templateConfigsRecord.templateVariableMapping).reduce((acc, [slotKey, slotEntry]) => {
340
+ const slotValue = slotEntry && typeof slotEntry === 'object' ? slotEntry.data : slotEntry;
341
+ if (slotValue != null) acc[slotKey] = slotValue;
342
+ return acc;
343
+ }, {})
344
+ : null;
345
+ const varMappedFromPayload = {
346
+ ...(varMapFromTemplateVariableMapping || {}),
347
+ ...(smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {}),
348
+ };
304
349
  const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
305
- const templateConfigsRecord =
306
- smsFallbackContent.templateConfigs && typeof smsFallbackContent.templateConfigs === 'object'
307
- ? smsFallbackContent.templateConfigs
308
- : {};
309
350
  const smsFallbackTemplateName =
310
351
  smsFallbackContent.templateName
311
352
  || smsFallbackContent.smsTemplateName