@capillarytech/creatives-library 9.0.35 → 9.0.36

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 (51) hide show
  1. package/app.js +175 -10
  2. package/entry.js +1 -67
  3. package/mfe-exposed-components.js +4 -2
  4. package/package.json +2 -2
  5. package/services/api.js +1 -9
  6. package/styles/containers/layout/_layoutPage.scss +6 -8
  7. package/utils/gtmTrackers/gtmEvents/creativeDetails.js +1 -2
  8. package/utils/rcsPayloadUtils.js +2 -5
  9. package/utils/tests/rcsPayloadUtils.test.js +2 -54
  10. package/v2Components/CapActionButton/constants.js +0 -1
  11. package/v2Components/CapActionButton/index.js +9 -77
  12. package/v2Components/CapActionButton/index.scss +0 -13
  13. package/v2Components/CapActionButton/messages.js +0 -13
  14. package/v2Components/CapActionButton/tests/index.test.js +1 -32
  15. package/v2Components/CommonTestAndPreview/index.js +15 -44
  16. package/v2Components/CommonTestAndPreview/tests/index.test.js +0 -78
  17. package/v2Components/CommonTestAndPreview/utils.js +0 -34
  18. package/v2Components/NavigationBar/index.js +7 -9
  19. package/v2Components/NavigationBar/tests/index.test.js +19 -39
  20. package/v2Components/SmsFallback/index.js +0 -6
  21. package/v2Containers/Cap/constants.js +0 -1
  22. package/v2Containers/Cap/index.js +27 -57
  23. package/v2Containers/CreativesContainer/index.js +8 -11
  24. package/v2Containers/CreativesContainer/tests/index.test.js +0 -53
  25. package/v2Containers/Rcs/carouselUtils.js +73 -46
  26. package/v2Containers/Rcs/components/CarouselCard.js +9 -11
  27. package/v2Containers/Rcs/components/CarouselCardButtons.js +0 -20
  28. package/v2Containers/Rcs/constants.js +7 -2
  29. package/v2Containers/Rcs/index.js +205 -226
  30. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +47 -60
  31. package/v2Containers/Rcs/tests/CarouselCard.test.js +2 -1
  32. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +0 -286
  33. package/v2Containers/Rcs/tests/carouselUtils.test.js +32 -28
  34. package/v2Containers/Rcs/tests/index.test.js +238 -151
  35. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +0 -110
  36. package/v2Containers/Rcs/tests/utils.test.js +17 -36
  37. package/v2Containers/Rcs/utils.js +55 -28
  38. package/v2Containers/SmsTrai/Edit/index.js +24 -35
  39. package/v2Containers/Templates/_templates.scss +1 -1
  40. package/v2Containers/Templates/index.js +0 -1
  41. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +0 -5
  42. package/v2Containers/TemplatesV2/TemplatesV2.style.js +3 -8
  43. package/v2Containers/TemplatesV2/index.js +9 -22
  44. package/AppRoot.js +0 -124
  45. package/app-config.js +0 -61
  46. package/bootstrap.js +0 -185
  47. package/utils/getDataLayer.js +0 -15
  48. package/utils/mfeDetect.js +0 -1
  49. package/utils/mfeFirstPaintReady.js +0 -29
  50. package/utils/mfeHistory.js +0 -62
  51. package/v2Components/NavigationBar/mfeModuleHeader.config.js +0 -16
@@ -71,7 +71,6 @@ import {
71
71
  mergeSmsFallbackForLibrary,
72
72
  buildSmsFallBackContentForPayload,
73
73
  pickRcsCardVarMappedEntries,
74
- extractRegisteredSenderIdsFromSmsFallbackRecord,
75
74
  } from './rcsLibraryHydrationUtils';
76
75
  import {
77
76
  RCS,
@@ -153,6 +152,7 @@ import { isTagIncluded } from '../../utils/commonUtils';
153
152
  import injectReducer from '../../utils/injectReducer';
154
153
  import v2RcsReducer from './reducer';
155
154
  import {
155
+ buildRcsNumericMustachePlaceholderRegex,
156
156
  getTemplateStatusType,
157
157
  normalizeCardVarMapped,
158
158
  coalesceCardVarMappedToTemplate,
@@ -175,7 +175,7 @@ import {
175
175
  getCarouselDescriptionCharacterCount,
176
176
  buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
177
177
  buildCarouselCardContentForPayload,
178
- getCarouselVarMapKey,
178
+ computeCarouselVarSlotKeys,
179
179
  } from './carouselUtils';
180
180
  import CarouselDimensionSelection from './components/CarouselDimensionSelection';
181
181
  import CarouselCard from './components/CarouselCard';
@@ -271,7 +271,12 @@ export const Rcs = (props) => {
271
271
  const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
272
272
  const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
273
273
  const [carouselResetNonce, setCarouselResetNonce] = useState(0);
274
- const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
274
+ // Title and description each track their own last-focused var slot (mirrors titleTextAreaId/
275
+ // descTextAreaId for the single-card flow) — a single shared id would let a click on the
276
+ // "Add Labels" picker next to one field silently apply to whichever field was focused last,
277
+ // if the user hadn't just clicked into that field's own var slot.
278
+ const [carouselTitleFocusedVarId, setCarouselTitleFocusedVarId] = useState('');
279
+ const [carouselDescFocusedVarId, setCarouselDescFocusedVarId] = useState('');
275
280
  const [imageError, setImageError] = useState(null);
276
281
  const [templateTitleError, setTemplateTitleError] = useState(false);
277
282
  const [cardVarMapped, setCardVarMapped] = useState({});
@@ -597,7 +602,8 @@ export const Rcs = (props) => {
597
602
  }}
598
603
  onTitleAddVar={() => appendVarToCarouselField(index, 'title')}
599
604
  onDescriptionAddVar={() => appendVarToCarouselField(index, 'description')}
600
- onTagSelect={onCarouselTagSelect}
605
+ onTitleTagSelect={onCarouselTitleTagSelect}
606
+ onDescTagSelect={onCarouselDescTagSelect}
601
607
  onContextChange={handleOnTagsContextChange}
602
608
  renderEditMessage={renderCarouselEditMessage}
603
609
  onFieldChange={(fieldName, value) => handleCarouselValueChange(index, [{ fieldName, value }])}
@@ -656,7 +662,8 @@ export const Rcs = (props) => {
656
662
  onChange={(key) => {
657
663
  setActiveCarouselIndex(`${key}`);
658
664
  // reset focused var when switching cards (as per requirement)
659
- setCarouselFocusedVarId('');
665
+ setCarouselTitleFocusedVarId('');
666
+ setCarouselDescFocusedVarId('');
660
667
  }}
661
668
  panes={getCarouselTabPanes()}
662
669
  />
@@ -776,7 +783,7 @@ export const Rcs = (props) => {
776
783
  ['title', 'description'].forEach((field) => {
777
784
  const templateStr = card?.[field] || '';
778
785
  if (!templateStr) return;
779
- const resolved = resolveCarouselTemplateWithMap(templateStr, idx);
786
+ const resolved = resolveCarouselTemplateWithMap(templateStr, idx, field === 'description');
780
787
  if (!resolved) {
781
788
  updateCarouselErrors(idx, { [field]: false });
782
789
  return;
@@ -869,17 +876,22 @@ export const Rcs = (props) => {
869
876
  };
870
877
 
871
878
  /**
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.
879
+ * Carousel resolve: each variable slot's key comes from `computeCarouselVarSlotKeys` (title,
880
+ * then description, per card; same name within a card shares one key; different cards never
881
+ * collide). A slot's key never depends on which tag currently fills it.
875
882
  */
876
- const resolveCarouselTemplateWithMap = (str = '', cardIndex) => {
883
+ const resolveCarouselTemplateWithMap = (str = '', cardIndex, isDescField = false) => {
877
884
  if (!str) return '';
878
885
  const arr = splitTemplateVarStringRcs(str);
886
+ const fieldKeys = (isDescField
887
+ ? computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex]?.description
888
+ : computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex]?.title) ?? [];
889
+ let varOrdinal = 0;
879
890
  return arr.map((elem) => {
880
891
  if (rcsVarTestRegex.test(elem)) {
881
- const varName = getVarNameFromToken(elem);
882
- const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, varName)];
892
+ const slotKey = fieldKeys[varOrdinal];
893
+ varOrdinal += 1;
894
+ const slotValue = cardVarMapped?.[slotKey];
883
895
  if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
884
896
  return String(slotValue);
885
897
  }
@@ -888,7 +900,7 @@ export const Rcs = (props) => {
888
900
  };
889
901
 
890
902
  const buildCarouselCardsForPreview = (cards = []) =>
891
- buildCarouselCardsForPreviewUtil(cards, { isFullMode, resolveCarouselTemplateWithMap });
903
+ buildCarouselCardsForPreviewUtil(cards, { isFullMode, rcsVarRegex, resolveTemplateWithMap });
892
904
 
893
905
  /**
894
906
  * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
@@ -1086,9 +1098,9 @@ export const Rcs = (props) => {
1086
1098
  if (mediaType) {
1087
1099
  setTemplateMediaType(mediaType);
1088
1100
  }
1089
- const tempOrientation = cardSettings?.cardOrientation;
1090
- const tempAlignment = cardSettings?.mediaAlignment;
1091
- const tempHeight = mediaData?.height;
1101
+ const tempOrientation = cardSettings.cardOrientation;
1102
+ const tempAlignment = cardSettings.mediaAlignment;
1103
+ const tempHeight = mediaData.height;
1092
1104
 
1093
1105
  // Map to actual RCS_IMAGE_DIMENSIONS keys
1094
1106
  let tempDimension;
@@ -1216,56 +1228,34 @@ export const Rcs = (props) => {
1216
1228
 
1217
1229
  setTemplateType(contentType.carousel);
1218
1230
  setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1219
-
1231
+ // Library/campaign payloads can store cardSettings/cardContent at the flat top-level
1232
+ // `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
1233
+ // used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
1234
+ // silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
1235
+ // actually uploaded at and causing object-fit:cover to crop the preview image.
1220
1236
  const cardSettings = rcsContent?.cardSettings
1221
1237
  || get(details, 'rcsContent.cardSettings', {});
1222
1238
  const cardWidth = cardSettings?.cardWidth || SMALL;
1223
1239
  setSelectedCarouselWidth(cardWidth);
1224
1240
 
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;
1241
+ const cards = Array.isArray(rcsContent?.cardContent)
1242
+ ? rcsContent.cardContent
1243
+ : get(details, 'rcsContent.cardContent', []);
1234
1244
  const firstHeight = cards?.[0]?.media?.height || MEDIUM;
1235
1245
  setSelectedCarouselHeight(firstHeight);
1236
1246
  setSelectedCarousel(`${firstHeight}_${cardWidth}`);
1237
1247
  setActiveCarouselIndex('0');
1238
1248
 
1239
1249
  if (cards.length > 1) {
1240
-
1241
- setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}, idx) => {
1250
+ // Each card's own `cardVarMapped` is already keyed by global var position (see
1251
+ // computeCarouselVarSlotKeys) a slot's key is assigned once, from where its token
1252
+ // sits in the template text, and never depends on which tag fills it. So merging every
1253
+ // card's own map directly is correct and collision-free: no per-card coalescing,
1254
+ // normalizing, or re-scoping needed (unlike the old name-based scoped-key scheme).
1255
+ setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}) => {
1242
1256
  const rawCardMap = c?.cardVarMapped;
1243
1257
  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 };
1258
+ return { ...merged, ...rawCardMap };
1269
1259
  }, { ...(previousVarMap || {}) }));
1270
1260
  }
1271
1261
 
@@ -1474,29 +1464,10 @@ export const Rcs = (props) => {
1474
1464
  globalActions.fetchSchemaForEntity(query);
1475
1465
  };
1476
1466
 
1477
- /**
1478
- * Replaces only the var token at `slotOrdinalZeroBased` (the field-local ordinal among
1479
- * `{{...}}` tokens, left to right) with `{{tagName}}`. Unlike a name-based regex replace,
1480
- * this targets a single slot even when the same tag name appears in other slots of the field.
1481
- */
1482
- const replaceTagAtFieldSlotOrdinal = (templateStr, slotOrdinalZeroBased, tagName) => {
1483
- if (!templateStr || slotOrdinalZeroBased === null || slotOrdinalZeroBased === undefined || !tagName) {
1484
- return templateStr;
1485
- }
1486
- let varOrdinal = 0;
1487
- let replaced = false;
1488
- const updatedSegments = splitTemplateVarStringRcs(templateStr).map((segment) => {
1489
- if (rcsVarTestRegex.test(segment)) {
1490
- const currentOrdinal = varOrdinal;
1491
- varOrdinal += 1;
1492
- if (currentOrdinal === slotOrdinalZeroBased) {
1493
- replaced = true;
1494
- return `{{${tagName}}}`;
1495
- }
1496
- }
1497
- return segment;
1498
- });
1499
- return replaced ? updatedSegments.join('') : templateStr;
1467
+ const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
1468
+ if (!templateStr || !numericVarName || !tagName) return templateStr;
1469
+ const placeholderRegex = buildRcsNumericMustachePlaceholderRegex(numericVarName);
1470
+ return templateStr.replace(placeholderRegex, `{{${tagName}}}`);
1500
1471
  };
1501
1472
 
1502
1473
  const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
@@ -1526,15 +1497,16 @@ export const Rcs = (props) => {
1526
1497
  setCardVarMapped((previousCardVarMapped) => {
1527
1498
  const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
1528
1499
  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.
1531
1500
  const existingValueBeforeAppend = (
1532
1501
  previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
1533
1502
  ).toString();
1534
1503
  const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1535
- updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
1504
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1505
+ updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
1536
1506
  } else {
1537
-
1507
+ // Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
1508
+ // "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
1509
+ // read/write the global numeric slot only and drop the shared semantic key.
1538
1510
  const existingValueBeforeAppend = cardVarMappedNumericSlotKey
1539
1511
  ? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
1540
1512
  : String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
@@ -1563,40 +1535,36 @@ export const Rcs = (props) => {
1563
1535
  return updatedCardVarMapped;
1564
1536
  });
1565
1537
 
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
-
1571
- if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC) {
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.
1575
- const fieldOffsetForSlot = tagAreaField === RCS_TAG_AREA_FIELD_TITLE
1576
- ? 0
1577
- : (templateTitle?.match(rcsVarRegex) ?? []).length;
1578
- const localSlotOrdinal = (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined)
1579
- ? globalVarSlotIndexZeroBased - fieldOffsetForSlot
1580
- : null;
1581
-
1538
+ if (
1539
+ isNumericPlaceholderSlot
1540
+ && (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
1541
+ ) {
1582
1542
  if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
1583
1543
  setTemplateTitle((previousTitle) => {
1584
- const titleAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal, selectedTagNameFromPicker);
1585
- if (titleAfterReplacingTag === previousTitle) return previousTitle;
1586
- setTemplateTitleError(computeTemplateTitleError(titleAfterReplacingTag));
1544
+ const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
1545
+ previousTitle || '',
1546
+ semanticOrNumericVarName,
1547
+ selectedTagNameFromPicker,
1548
+ );
1549
+ if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
1550
+ setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
1587
1551
  // 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
1588
1552
  setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
1589
- return titleAfterReplacingTag;
1553
+ return titleAfterReplacingNumericPlaceholder;
1590
1554
  });
1591
1555
  } else {
1592
1556
  setTemplateDesc((previousDescription) => {
1593
- const descriptionAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal, selectedTagNameFromPicker);
1594
- if (descriptionAfterReplacingTag === previousDescription) {
1557
+ const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
1558
+ previousDescription || '',
1559
+ semanticOrNumericVarName,
1560
+ selectedTagNameFromPicker,
1561
+ );
1562
+ if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
1595
1563
  return previousDescription;
1596
1564
  }
1597
- setTemplateDescError(computeTemplateDescError(descriptionAfterReplacingTag));
1565
+ setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
1598
1566
  setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
1599
- return descriptionAfterReplacingTag;
1567
+ return descriptionAfterReplacingNumericPlaceholder;
1600
1568
  });
1601
1569
  }
1602
1570
  }
@@ -1606,25 +1574,51 @@ export const Rcs = (props) => {
1606
1574
 
1607
1575
  const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
1608
1576
 
1609
- const onCarouselTagSelect = (data) => {
1610
- if (!carouselFocusedVarId) return;
1611
- const sep = carouselFocusedVarId.lastIndexOf('_');
1612
- if (sep === -1) return;
1613
- const token = carouselFocusedVarId.slice(0, sep);
1614
- const variableName = getVarNameFromToken(token);
1615
- if (!variableName) return;
1577
+ /** Field-local var ordinal (0-based, vars only) for a `{{tok}}_segIdx` composite id — scoped to
1578
+ * a single field's own template string (title and description are tracked independently). */
1579
+ const getCarouselFieldLocalVarOrdinal = (compositeId, fieldTemplateStr) => {
1580
+ const segments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
1581
+ let varOrdinal = 0;
1582
+ for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
1583
+ if (rcsVarTestRegex.test(segments[segmentIndex])) {
1584
+ if (`${segments[segmentIndex]}_${segmentIndex}` === compositeId) return varOrdinal;
1585
+ varOrdinal += 1;
1586
+ }
1587
+ }
1588
+ return null;
1589
+ };
1590
+
1591
+ // The card's placeholder (numeric {{N}} or a bare semantic token like {{first_name}}) is locked
1592
+ // here — picking a tag only supplies/replaces the *value* behind that slot in cardVarMapped, it
1593
+ // never rewrites the card's title/description text. `focusedVarId` is passed in by the caller
1594
+ // (title vs description each track their own last-focused slot, like titleTextAreaId/
1595
+ // descTextAreaId) so a click on one field's "Add Labels" can never apply to the other field's
1596
+ // last-focused slot. The slot's key is its global position across the whole carousel — assigned
1597
+ // once from where its token sits in the template text, never from whatever tag currently fills
1598
+ // it — so swapping tags never changes the key and never collides with another card's slot.
1599
+ const onCarouselTagSelect = (data, focusedVarId, isDescField) => {
1600
+ if (!focusedVarId) return;
1616
1601
  const activeIdx = parseInt(activeCarouselIndex, 10);
1617
- const mapKey = getCarouselVarMapKey(isNaN(activeIdx) ? 0 : activeIdx, variableName);
1618
- setCardVarMapped((prev) => {
1619
- const base = (prev?.[mapKey] ?? '').toString();
1620
- const nextVal = `${base}{{${data}}}`;
1621
- return {
1622
- ...(prev || {}),
1623
- [mapKey]: nextVal,
1624
- };
1625
- });
1602
+ const cardIndex = isNaN(activeIdx) ? 0 : activeIdx;
1603
+ const fieldTemplateStr = (isDescField
1604
+ ? carouselData?.[cardIndex]?.description
1605
+ : carouselData?.[cardIndex]?.title) || '';
1606
+ const localVarOrdinal = getCarouselFieldLocalVarOrdinal(focusedVarId, fieldTemplateStr);
1607
+ if (localVarOrdinal == null) return;
1608
+ const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
1609
+ const slotKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
1610
+ if (!slotKey) return;
1611
+
1612
+ setCardVarMapped((prev) => ({
1613
+ ...(prev || {}),
1614
+ [slotKey]: `{{${data}}}`,
1615
+ }));
1626
1616
  };
1627
1617
 
1618
+ const onCarouselTitleTagSelect = (data) => onCarouselTagSelect(data, carouselTitleFocusedVarId, false);
1619
+
1620
+ const onCarouselDescTagSelect = (data) => onCarouselTagSelect(data, carouselDescFocusedVarId, true);
1621
+
1628
1622
  //removing optout tag for rcs
1629
1623
  const getRcsTags = () => {
1630
1624
  const tempTags = cloneDeep(tags);
@@ -1710,7 +1704,7 @@ export const Rcs = (props) => {
1710
1704
  key={`${elem}_${index}`}
1711
1705
  placeholder={`enter the value for ${elem}`}
1712
1706
  autosize={{ minRows: 1, maxRows: 3 }}
1713
- onChange={e => textAreaValueChange(e, type)}
1707
+ onChange={e => textAreaValueChange(e)}
1714
1708
  value={textAreaValue(index, type)}
1715
1709
  onFocus={(e) => setTextAreaId(e, type)}
1716
1710
  />
@@ -1732,13 +1726,31 @@ export const Rcs = (props) => {
1732
1726
  return renderArray;
1733
1727
  };
1734
1728
  const onTemplateTitleChange = ({ target: { value } }) => {
1729
+ let errorMessage = false;
1730
+ if (templateType === contentType.rich_card && !value.trim()) {
1731
+ errorMessage = formatMessage(messages.emptyTemplateTitleErrorMessage);
1732
+ } else if (value.length > TEMPLATE_TITLE_MAX_LENGTH) {
1733
+ errorMessage = formatMessage(messages.templateHeaderLengthError);
1734
+ } else {
1735
+ errorMessage = variableErrorHandling(value);
1736
+ }
1735
1737
  setTemplateTitle(value);
1736
- setTemplateTitleError(computeTemplateTitleError(value));
1738
+ setTemplateTitleError(errorMessage);
1737
1739
  };
1738
1740
 
1739
1741
  const onTemplateDescChange = ({ target: { value } }) => {
1742
+ let errorMessage = false;
1743
+ if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
1744
+ errorMessage = formatMessage(messages.templateMessageLengthError);
1745
+ } else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
1746
+ errorMessage = formatMessage(messages.templateMessageLengthError);
1747
+ } else {
1748
+ errorMessage = false;
1749
+ }
1750
+ const varError = variableErrorHandling(value);
1751
+ const error = errorMessage || varError;
1740
1752
  setTemplateDesc(value);
1741
- setTemplateDescError(computeTemplateDescError(value));
1753
+ setTemplateDescError(error);
1742
1754
  };
1743
1755
 
1744
1756
 
@@ -1821,29 +1833,6 @@ export const Rcs = (props) => {
1821
1833
  return false;
1822
1834
  };
1823
1835
 
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
-
1847
1836
  const onMessageAddVar = () => {
1848
1837
  onAddVar(templateDesc);
1849
1838
  };
@@ -1930,31 +1919,37 @@ const onTitleAddVar = () => {
1930
1919
 
1931
1920
  // Carousel: render variable-value editor for a given template string (title/description) belonging
1932
1921
  // 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) => {
1922
+ // tokens are editable. Each slot's key comes from `computeCarouselVarSlotKeys` stable regardless
1923
+ // of which tag fills it, and independent even from another occurrence of the same tag name.
1924
+ const renderCarouselEditMessage = (templateStr, cardIndex, fieldType) => {
1936
1925
  const renderArray = [];
1937
1926
  const templateArr = splitTemplateVarString(templateStr);
1927
+ const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
1928
+ const slotKeysForCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
1929
+ const fieldKeys = (isDescField ? slotKeysForCard?.description : slotKeysForCard?.title) ?? [];
1930
+ let varOrdinal = 0;
1938
1931
  if (templateArr?.length) {
1939
1932
  templateArr.forEach((elem, index) => {
1940
1933
  if (rcsVarTestRegex.test(elem)) {
1941
1934
  const varName = getVarNameFromToken(elem);
1942
- const mapKey = getCarouselVarMapKey(cardIndex, varName);
1935
+ const mapKey = fieldKeys[varOrdinal];
1936
+ varOrdinal += 1;
1943
1937
  const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
1944
1938
  renderArray.push(
1945
- <div
1946
- key={`${elem}_${index}_${rcsVarSegmentEditorRemountKey}`}
1947
- className="var-segment-message-editor__var-slot"
1948
- >
1939
+ <div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
1949
1940
  <TextArea
1950
1941
  id={`${elem}_${index}`}
1951
1942
  placeholder={`enter the value for ${elem}`}
1952
1943
  autosize={{ minRows: 1, maxRows: 3 }}
1953
- onChange={(e) => textAreaValueChange(e, cardIndex)}
1944
+ onChange={(e) => carouselTextAreaValueChange(e, cardIndex, fieldType)}
1954
1945
  value={computedValue}
1955
1946
  onFocus={(e) => {
1956
1947
  const id = e?.target?.id || e?.currentTarget?.id || '';
1957
- setCarouselFocusedVarId(id);
1948
+ if (fieldType === RCS_TAG_AREA_FIELD_DESC) {
1949
+ setCarouselDescFocusedVarId(id);
1950
+ } else {
1951
+ setCarouselTitleFocusedVarId(id);
1952
+ }
1958
1953
  }}
1959
1954
  />
1960
1955
  </div>
@@ -1975,7 +1970,7 @@ const onTitleAddVar = () => {
1975
1970
  return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
1976
1971
  };
1977
1972
 
1978
- const textAreaValueChange = (e, cardIndex) => {
1973
+ const textAreaValueChange = (e) => {
1979
1974
  const value = e?.target?.value ?? '';
1980
1975
  const id = e?.target?.id || e?.currentTarget?.id || '';
1981
1976
  if (!id) return;
@@ -1986,14 +1981,38 @@ const onTitleAddVar = () => {
1986
1981
  const variableName = getVarNameFromToken(token);
1987
1982
 
1988
1983
  if (variableName) {
1989
- const mapKey = getCarouselVarMapKey(cardIndex, variableName);
1984
+ const nextVal = isInvalidValue ? "" : value;
1990
1985
  setCardVarMapped((prev) => ({
1991
1986
  ...prev,
1992
- [mapKey]: isInvalidValue ? "" : value,
1987
+ [variableName]: nextVal,
1993
1988
  }));
1994
1989
  }
1995
1990
  };
1996
1991
 
1992
+ // Carousel: typing directly into a slot's value box (as opposed to picking a tag) — same global
1993
+ // positional key as onCarouselTagSelect/renderCarouselEditMessage, so both ways of filling a
1994
+ // slot land on the identical key.
1995
+ const carouselTextAreaValueChange = (e, cardIndex, fieldType) => {
1996
+ const value = e?.target?.value ?? '';
1997
+ const id = e?.target?.id || e?.currentTarget?.id || '';
1998
+ if (!id) return;
1999
+ const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
2000
+ const fieldTemplateStr = (isDescField
2001
+ ? carouselData?.[cardIndex]?.description
2002
+ : carouselData?.[cardIndex]?.title) || '';
2003
+ const localVarOrdinal = getCarouselFieldLocalVarOrdinal(id, fieldTemplateStr);
2004
+ if (localVarOrdinal == null) return;
2005
+ const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
2006
+ const mapKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
2007
+ if (!mapKey) return;
2008
+ const isInvalidValue = value?.trim() === "";
2009
+ const nextVal = isInvalidValue ? "" : value;
2010
+ setCardVarMapped((prev) => ({
2011
+ ...prev,
2012
+ [mapKey]: nextVal,
2013
+ }));
2014
+ };
2015
+
1997
2016
  const setTextAreaId = (e, type) => {
1998
2017
  // VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
1999
2018
  // have an `.target.id` shape. Support both.
@@ -2031,11 +2050,6 @@ const onTitleAddVar = () => {
2031
2050
  isFullMode={isFullMode}
2032
2051
  maxButtons={MAX_BUTTONS}
2033
2052
  host={hostName}
2034
- location={location}
2035
- tags={getRcsTags()}
2036
- injectedTags={injectedTags || {}}
2037
- selectedOfferDetails={selectedOfferDetails}
2038
- onContextChange={handleOnTagsContextChange}
2039
2053
  />
2040
2054
  </>
2041
2055
  );
@@ -2657,6 +2671,8 @@ const onTitleAddVar = () => {
2657
2671
  const carouselVidDims =
2658
2672
  RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
2659
2673
  || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
2674
+ // Debug log for embedded/library mode preview payload (carousel)
2675
+ // eslint-disable-next-line no-console
2660
2676
  return (
2661
2677
  <UnifiedPreview
2662
2678
  channel={RCS}
@@ -2840,55 +2856,40 @@ const onTitleAddVar = () => {
2840
2856
  const cardContent = rcsForTest.rcsContent?.cardContent;
2841
2857
  if (Array.isArray(cardContent) && cardContent[0]) {
2842
2858
  if (isCarouselType) {
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:
2859
+ // Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
2860
+ // user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
2845
2861
  // - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
2846
2862
  // - prepareTagExtractionPayload can extract tag metadata from the card content.
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).
2863
+ // Each variable slot's key comes from computeCarouselVarSlotKeys, so two cards' variables
2864
+ // never share a key even when they pick the same tag name for their own slot.
2850
2865
  rcsForTest = {
2851
2866
  ...rcsForTest,
2852
2867
  rcsContent: {
2853
2868
  ...rcsForTest.rcsContent,
2854
- cardContent: cardContent.map((card, cardIndex) => {
2869
+ cardContent: cardContent.map((card) => {
2855
2870
  const rawTitle = card.title || '';
2856
2871
  const rawDesc = card.description || '';
2857
- const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex);
2858
- const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex);
2872
+ const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex, false);
2873
+ const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex, true);
2859
2874
  return { ...card, title: resolvedTitle, description: resolvedDesc };
2860
2875
  }),
2861
2876
  },
2862
2877
  };
2863
2878
  } else if (isSlotMappingModeForPreview) {
2864
2879
  // Standalone card: coalesce cardVarMapped with the template's slot names so the preview
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.
2880
+ // API receives a correctly-keyed var map for non-carousel templates.
2870
2881
  const fullCardVarMapped = coalesceCardVarMappedToTemplate(
2871
2882
  pickRcsCardVarMappedEntries(cardVarMapped),
2872
2883
  templateTitle,
2873
2884
  templateDesc,
2874
2885
  rcsVarRegex,
2875
2886
  );
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);
2881
2887
  rcsForTest = {
2882
2888
  ...rcsForTest,
2883
2889
  rcsContent: {
2884
2890
  ...rcsForTest.rcsContent,
2885
2891
  cardContent: [
2886
- {
2887
- ...cardContent[0],
2888
- title: resolvedStandaloneTitle,
2889
- description: resolvedStandaloneDesc,
2890
- cardVarMapped: fullCardVarMapped,
2891
- },
2892
+ { ...cardContent[0], cardVarMapped: fullCardVarMapped },
2892
2893
  ...cardContent.slice(1),
2893
2894
  ],
2894
2895
  },
@@ -2904,23 +2905,16 @@ const onTitleAddVar = () => {
2904
2905
  },
2905
2906
  },
2906
2907
  };
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) {
2908
+ // Use the smsFallbackTemplateConfigs already built by createPayload() / buildSmsFallBackContentForPayload —
2909
+ // it merges the API baseline (templateId, registeredSenderIds, templateName, etc.) with local
2910
+ // smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced smsTemplateId
2911
+ // in campaign mode. This also keeps templateName (and all other fields) in sync with the approval
2912
+ // payload automatically.
2913
+ const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
2914
+ if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
2919
2915
  out.templateConfigs = {
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,
2916
+ ...smsFallbackTcFromPayload,
2917
+ traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
2924
2918
  };
2925
2919
  }
2926
2920
  return out;
@@ -3066,12 +3060,16 @@ const onTitleAddVar = () => {
3066
3060
  ['title', 'description'].some((field) => !(card?.[field] || '').trim())
3067
3061
  );
3068
3062
  if (hasEmptyField) return true;
3063
+ const slotKeysByCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex);
3069
3064
  const unfilledVar = (carouselData || []).some((card, cardIndex) =>
3070
3065
  ['title', 'description'].some((field) => {
3066
+ const isDescField = field === 'description';
3071
3067
  const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
3072
- return tokens.some((token) => {
3073
- const name = token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
3074
- const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, name)];
3068
+ if (tokens.length === 0) return false;
3069
+ const fieldKeys = (isDescField ? slotKeysByCard[cardIndex]?.description : slotKeysByCard[cardIndex]?.title) ?? [];
3070
+ return tokens.some((token, localVarOrdinal) => {
3071
+ const slotKey = fieldKeys[localVarOrdinal];
3072
+ const slotValue = cardVarMapped?.[slotKey];
3075
3073
  return slotValue == null || String(slotValue).trim() === '';
3076
3074
  });
3077
3075
  })
@@ -3079,19 +3077,6 @@ const onTitleAddVar = () => {
3079
3077
  return unfilledVar;
3080
3078
  };
3081
3079
 
3082
- // Mirror WhatsApp's isEditDoneDisabled: block Done while a CTA button's dynamic URL still
3083
- // holds the unresolved {{1}} placeholder (no real personalization tag assigned yet).
3084
- const hasUnresolvedDynamicCtaUrl = () =>
3085
- isCarouselType
3086
- ? (carouselData || []).some((card) =>
3087
- (card?.suggestions || []).some((suggestion) =>
3088
- suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
3089
- )
3090
- )
3091
- : suggestions.some((suggestion) =>
3092
- suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
3093
- );
3094
-
3095
3080
  const isDisableDone = () => {
3096
3081
  if(isEditFlow){
3097
3082
  return false;
@@ -3133,9 +3118,6 @@ const onTitleAddVar = () => {
3133
3118
  return true;
3134
3119
  }
3135
3120
  }
3136
- if (hasUnresolvedDynamicCtaUrl()) {
3137
- return true;
3138
- }
3139
3121
  if (templateDescError || templateTitleError) {
3140
3122
  return true;
3141
3123
  }
@@ -3180,9 +3162,6 @@ const onTitleAddVar = () => {
3180
3162
  return true;
3181
3163
  }
3182
3164
  }
3183
- if (hasUnresolvedDynamicCtaUrl()) {
3184
- return true;
3185
- }
3186
3165
  if (templateTitleError || templateDescError) {
3187
3166
  return true;
3188
3167
  }