@capillarytech/creatives-library 9.0.36 → 9.0.37-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AppRoot.js +124 -0
- package/app-config.js +61 -0
- package/app.js +10 -175
- package/bootstrap.js +185 -0
- package/entry.js +67 -1
- package/mfe-exposed-components.js +2 -4
- package/package.json +2 -2
- package/services/api.js +9 -1
- package/styles/containers/layout/_layoutPage.scss +8 -6
- package/utils/getDataLayer.js +15 -0
- package/utils/gtmTrackers/gtmEvents/creativeDetails.js +2 -1
- package/utils/mfeDetect.js +1 -0
- package/utils/mfeFirstPaintReady.js +29 -0
- package/utils/mfeHistory.js +62 -0
- package/utils/rcsPayloadUtils.js +5 -2
- package/utils/tests/rcsPayloadUtils.test.js +54 -2
- package/v2Components/CapActionButton/constants.js +1 -0
- package/v2Components/CapActionButton/index.js +77 -9
- package/v2Components/CapActionButton/index.scss +13 -0
- package/v2Components/CapActionButton/messages.js +13 -0
- package/v2Components/CapActionButton/tests/index.test.js +32 -1
- package/v2Components/CommonTestAndPreview/index.js +44 -15
- package/v2Components/CommonTestAndPreview/tests/index.test.js +78 -0
- package/v2Components/CommonTestAndPreview/utils.js +34 -0
- package/v2Components/NavigationBar/index.js +9 -7
- package/v2Components/NavigationBar/mfeModuleHeader.config.js +16 -0
- package/v2Components/NavigationBar/tests/index.test.js +39 -19
- package/v2Components/SmsFallback/index.js +6 -0
- package/v2Containers/Cap/constants.js +1 -0
- package/v2Containers/Cap/index.js +57 -27
- package/v2Containers/CreativesContainer/index.js +11 -8
- package/v2Containers/CreativesContainer/tests/index.test.js +53 -0
- package/v2Containers/Rcs/components/CarouselCard.js +5 -0
- package/v2Containers/Rcs/components/CarouselCardButtons.js +20 -0
- package/v2Containers/Rcs/index.js +154 -79
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +55 -4
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +286 -0
- package/v2Containers/Rcs/tests/carouselUtils.test.js +13 -17
- package/v2Containers/Rcs/tests/index.test.js +141 -6
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +110 -0
- package/v2Containers/Rcs/tests/utils.test.js +36 -0
- package/v2Containers/Rcs/utils.js +16 -4
- package/v2Containers/SmsTrai/Edit/index.js +35 -24
- package/v2Containers/Templates/_templates.scss +1 -1
- package/v2Containers/Templates/index.js +1 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +8 -3
- package/v2Containers/TemplatesV2/index.js +22 -9
|
@@ -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,
|
|
@@ -900,7 +900,7 @@ export const Rcs = (props) => {
|
|
|
900
900
|
};
|
|
901
901
|
|
|
902
902
|
const buildCarouselCardsForPreview = (cards = []) =>
|
|
903
|
-
buildCarouselCardsForPreviewUtil(cards, { isFullMode,
|
|
903
|
+
buildCarouselCardsForPreviewUtil(cards, { isFullMode, resolveCarouselTemplateWithMap });
|
|
904
904
|
|
|
905
905
|
/**
|
|
906
906
|
* Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
|
|
@@ -1098,9 +1098,9 @@ export const Rcs = (props) => {
|
|
|
1098
1098
|
if (mediaType) {
|
|
1099
1099
|
setTemplateMediaType(mediaType);
|
|
1100
1100
|
}
|
|
1101
|
-
const tempOrientation = cardSettings
|
|
1102
|
-
const tempAlignment = cardSettings
|
|
1103
|
-
const tempHeight = mediaData
|
|
1101
|
+
const tempOrientation = cardSettings?.cardOrientation;
|
|
1102
|
+
const tempAlignment = cardSettings?.mediaAlignment;
|
|
1103
|
+
const tempHeight = mediaData?.height;
|
|
1104
1104
|
|
|
1105
1105
|
// Map to actual RCS_IMAGE_DIMENSIONS keys
|
|
1106
1106
|
let tempDimension;
|
|
@@ -1228,19 +1228,21 @@ export const Rcs = (props) => {
|
|
|
1228
1228
|
|
|
1229
1229
|
setTemplateType(contentType.carousel);
|
|
1230
1230
|
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1231
|
-
|
|
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.
|
|
1231
|
+
|
|
1236
1232
|
const cardSettings = rcsContent?.cardSettings
|
|
1237
1233
|
|| get(details, 'rcsContent.cardSettings', {});
|
|
1238
1234
|
const cardWidth = cardSettings?.cardWidth || SMALL;
|
|
1239
1235
|
setSelectedCarouselWidth(cardWidth);
|
|
1240
1236
|
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1237
|
+
const nestedCards = Array.isArray(rcsContent?.cardContent) ? rcsContent.cardContent : [];
|
|
1238
|
+
const topCardsRaw = get(details, 'rcsContent.cardContent', []);
|
|
1239
|
+
const topCards = Array.isArray(topCardsRaw) ? topCardsRaw : [];
|
|
1240
|
+
const cardCount = Math.max(nestedCards.length, topCards.length);
|
|
1241
|
+
const mergedCards = Array.from({ length: cardCount }, (_, idx) => ({
|
|
1242
|
+
...(topCards[idx] || {}),
|
|
1243
|
+
...(nestedCards[idx] || {}),
|
|
1244
|
+
}));
|
|
1245
|
+
const cards = cardCount > 0 ? mergedCards : topCards;
|
|
1244
1246
|
const firstHeight = cards?.[0]?.media?.height || MEDIUM;
|
|
1245
1247
|
setSelectedCarouselHeight(firstHeight);
|
|
1246
1248
|
setSelectedCarousel(`${firstHeight}_${cardWidth}`);
|
|
@@ -1464,10 +1466,29 @@ export const Rcs = (props) => {
|
|
|
1464
1466
|
globalActions.fetchSchemaForEntity(query);
|
|
1465
1467
|
};
|
|
1466
1468
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1469
|
+
/**
|
|
1470
|
+
* Replaces only the var token at `slotOrdinalZeroBased` (the field-local ordinal among
|
|
1471
|
+
* `{{...}}` tokens, left to right) with `{{tagName}}`. Unlike a name-based regex replace,
|
|
1472
|
+
* this targets a single slot even when the same tag name appears in other slots of the field.
|
|
1473
|
+
*/
|
|
1474
|
+
const replaceTagAtFieldSlotOrdinal = (templateStr, slotOrdinalZeroBased, tagName) => {
|
|
1475
|
+
if (!templateStr || slotOrdinalZeroBased === null || slotOrdinalZeroBased === undefined || !tagName) {
|
|
1476
|
+
return templateStr;
|
|
1477
|
+
}
|
|
1478
|
+
let varOrdinal = 0;
|
|
1479
|
+
let replaced = false;
|
|
1480
|
+
const updatedSegments = splitTemplateVarStringRcs(templateStr).map((segment) => {
|
|
1481
|
+
if (rcsVarTestRegex.test(segment)) {
|
|
1482
|
+
const currentOrdinal = varOrdinal;
|
|
1483
|
+
varOrdinal += 1;
|
|
1484
|
+
if (currentOrdinal === slotOrdinalZeroBased) {
|
|
1485
|
+
replaced = true;
|
|
1486
|
+
return `{{${tagName}}}`;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return segment;
|
|
1490
|
+
});
|
|
1491
|
+
return replaced ? updatedSegments.join('') : templateStr;
|
|
1471
1492
|
};
|
|
1472
1493
|
|
|
1473
1494
|
const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
|
|
@@ -1497,16 +1518,15 @@ export const Rcs = (props) => {
|
|
|
1497
1518
|
setCardVarMapped((previousCardVarMapped) => {
|
|
1498
1519
|
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
1499
1520
|
if (isNumericPlaceholderSlot) {
|
|
1521
|
+
// Numeric slot ({{1}}, {{2}}, …) is the persisted API contract — keep the numeric key
|
|
1522
|
+
// itself (do not fork off a semantic-name key); the template text stays {{N}} below.
|
|
1500
1523
|
const existingValueBeforeAppend = (
|
|
1501
1524
|
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
1502
1525
|
).toString();
|
|
1503
1526
|
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1504
|
-
|
|
1505
|
-
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
1527
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
1506
1528
|
} else {
|
|
1507
|
-
|
|
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.
|
|
1529
|
+
|
|
1510
1530
|
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
1511
1531
|
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
1512
1532
|
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
@@ -1535,36 +1555,40 @@ export const Rcs = (props) => {
|
|
|
1535
1555
|
return updatedCardVarMapped;
|
|
1536
1556
|
});
|
|
1537
1557
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
)
|
|
1558
|
+
// Numeric slot ({{1}}, {{2}}, …): the token is already the persisted API contract — leave
|
|
1559
|
+
// templateTitle/templateDesc untouched. The chip label re-resolves via titleVarSegmentValueMapById
|
|
1560
|
+
// / descriptionVarSegmentValueMapById, both keyed off cardVarMapped (updated above).
|
|
1561
|
+
if (isNumericPlaceholderSlot) return;
|
|
1562
|
+
|
|
1563
|
+
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC) {
|
|
1564
|
+
// Legacy slot that already holds a named tag (e.g. {{first_name}}) is replaced by
|
|
1565
|
+
// field-local position, since a name-based replace would also overwrite other slots
|
|
1566
|
+
// sharing that same tag name.
|
|
1567
|
+
const fieldOffsetForSlot = tagAreaField === RCS_TAG_AREA_FIELD_TITLE
|
|
1568
|
+
? 0
|
|
1569
|
+
: (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
1570
|
+
const localSlotOrdinal = (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined)
|
|
1571
|
+
? globalVarSlotIndexZeroBased - fieldOffsetForSlot
|
|
1572
|
+
: null;
|
|
1573
|
+
|
|
1542
1574
|
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
1543
1575
|
setTemplateTitle((previousTitle) => {
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
selectedTagNameFromPicker,
|
|
1548
|
-
);
|
|
1549
|
-
if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
|
|
1550
|
-
setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
|
|
1576
|
+
const titleAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal, selectedTagNameFromPicker);
|
|
1577
|
+
if (titleAfterReplacingTag === previousTitle) return previousTitle;
|
|
1578
|
+
setTemplateTitleError(computeTemplateTitleError(titleAfterReplacingTag));
|
|
1551
1579
|
// 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
|
|
1552
1580
|
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1553
|
-
return
|
|
1581
|
+
return titleAfterReplacingTag;
|
|
1554
1582
|
});
|
|
1555
1583
|
} else {
|
|
1556
1584
|
setTemplateDesc((previousDescription) => {
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
semanticOrNumericVarName,
|
|
1560
|
-
selectedTagNameFromPicker,
|
|
1561
|
-
);
|
|
1562
|
-
if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
|
|
1585
|
+
const descriptionAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal, selectedTagNameFromPicker);
|
|
1586
|
+
if (descriptionAfterReplacingTag === previousDescription) {
|
|
1563
1587
|
return previousDescription;
|
|
1564
1588
|
}
|
|
1565
|
-
setTemplateDescError(
|
|
1589
|
+
setTemplateDescError(computeTemplateDescError(descriptionAfterReplacingTag));
|
|
1566
1590
|
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1567
|
-
return
|
|
1591
|
+
return descriptionAfterReplacingTag;
|
|
1568
1592
|
});
|
|
1569
1593
|
}
|
|
1570
1594
|
}
|
|
@@ -1726,31 +1750,13 @@ export const Rcs = (props) => {
|
|
|
1726
1750
|
return renderArray;
|
|
1727
1751
|
};
|
|
1728
1752
|
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
|
-
}
|
|
1737
1753
|
setTemplateTitle(value);
|
|
1738
|
-
setTemplateTitleError(
|
|
1754
|
+
setTemplateTitleError(computeTemplateTitleError(value));
|
|
1739
1755
|
};
|
|
1740
1756
|
|
|
1741
1757
|
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;
|
|
1752
1758
|
setTemplateDesc(value);
|
|
1753
|
-
setTemplateDescError(
|
|
1759
|
+
setTemplateDescError(computeTemplateDescError(value));
|
|
1754
1760
|
};
|
|
1755
1761
|
|
|
1756
1762
|
|
|
@@ -1833,6 +1839,29 @@ export const Rcs = (props) => {
|
|
|
1833
1839
|
return false;
|
|
1834
1840
|
};
|
|
1835
1841
|
|
|
1842
|
+
// Shared with onTemplateTitleChange so tag-picker inserts enforce the same length limit as manual typing.
|
|
1843
|
+
const computeTemplateTitleError = (value) => {
|
|
1844
|
+
if (templateType === contentType.rich_card && !value.trim()) {
|
|
1845
|
+
return formatMessage(messages.emptyTemplateTitleErrorMessage);
|
|
1846
|
+
}
|
|
1847
|
+
if (value.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
1848
|
+
return formatMessage(messages.templateHeaderLengthError);
|
|
1849
|
+
}
|
|
1850
|
+
return variableErrorHandling(value);
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
// Shared with onTemplateDescChange so tag-picker inserts enforce the same length limit as manual typing.
|
|
1854
|
+
const computeTemplateDescError = (value) => {
|
|
1855
|
+
let errorMessage = false;
|
|
1856
|
+
if (templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)) {
|
|
1857
|
+
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1858
|
+
} else if (templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH) {
|
|
1859
|
+
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1860
|
+
}
|
|
1861
|
+
const varError = variableErrorHandling(value);
|
|
1862
|
+
return errorMessage || varError;
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1836
1865
|
const onMessageAddVar = () => {
|
|
1837
1866
|
onAddVar(templateDesc);
|
|
1838
1867
|
};
|
|
@@ -1936,7 +1965,10 @@ const onTitleAddVar = () => {
|
|
|
1936
1965
|
varOrdinal += 1;
|
|
1937
1966
|
const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
|
|
1938
1967
|
renderArray.push(
|
|
1939
|
-
<div
|
|
1968
|
+
<div
|
|
1969
|
+
key={`${elem}_${index}_${rcsVarSegmentEditorRemountKey}`}
|
|
1970
|
+
className="var-segment-message-editor__var-slot"
|
|
1971
|
+
>
|
|
1940
1972
|
<TextArea
|
|
1941
1973
|
id={`${elem}_${index}`}
|
|
1942
1974
|
placeholder={`enter the value for ${elem}`}
|
|
@@ -2050,6 +2082,11 @@ const onTitleAddVar = () => {
|
|
|
2050
2082
|
isFullMode={isFullMode}
|
|
2051
2083
|
maxButtons={MAX_BUTTONS}
|
|
2052
2084
|
host={hostName}
|
|
2085
|
+
location={location}
|
|
2086
|
+
tags={getRcsTags()}
|
|
2087
|
+
injectedTags={injectedTags || {}}
|
|
2088
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
2089
|
+
onContextChange={handleOnTagsContextChange}
|
|
2053
2090
|
/>
|
|
2054
2091
|
</>
|
|
2055
2092
|
);
|
|
@@ -2671,8 +2708,6 @@ const onTitleAddVar = () => {
|
|
|
2671
2708
|
const carouselVidDims =
|
|
2672
2709
|
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
2673
2710
|
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
2674
|
-
// Debug log for embedded/library mode preview payload (carousel)
|
|
2675
|
-
// eslint-disable-next-line no-console
|
|
2676
2711
|
return (
|
|
2677
2712
|
<UnifiedPreview
|
|
2678
2713
|
channel={RCS}
|
|
@@ -2856,8 +2891,8 @@ const onTitleAddVar = () => {
|
|
|
2856
2891
|
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
2857
2892
|
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
2858
2893
|
if (isCarouselType) {
|
|
2859
|
-
// Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values
|
|
2860
|
-
// user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
2894
|
+
// Carousel: resolve {{N}}/named slot tokens to the actual tag expressions / static values
|
|
2895
|
+
// the user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
2861
2896
|
// - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
|
|
2862
2897
|
// - prepareTagExtractionPayload can extract tag metadata from the card content.
|
|
2863
2898
|
// Each variable slot's key comes from computeCarouselVarSlotKeys, so two cards' variables
|
|
@@ -2866,7 +2901,7 @@ const onTitleAddVar = () => {
|
|
|
2866
2901
|
...rcsForTest,
|
|
2867
2902
|
rcsContent: {
|
|
2868
2903
|
...rcsForTest.rcsContent,
|
|
2869
|
-
cardContent: cardContent.map((card) => {
|
|
2904
|
+
cardContent: cardContent.map((card, cardIndex) => {
|
|
2870
2905
|
const rawTitle = card.title || '';
|
|
2871
2906
|
const rawDesc = card.description || '';
|
|
2872
2907
|
const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex, false);
|
|
@@ -2877,19 +2912,33 @@ const onTitleAddVar = () => {
|
|
|
2877
2912
|
};
|
|
2878
2913
|
} else if (isSlotMappingModeForPreview) {
|
|
2879
2914
|
// Standalone card: coalesce cardVarMapped with the template's slot names so the preview
|
|
2880
|
-
// API receives a correctly-keyed var map for non-carousel templates.
|
|
2915
|
+
// API receives a correctly-keyed var map for non-carousel templates. Also resolve the
|
|
2916
|
+
// {{N}} slot tokens in title/description themselves (mirrors the carousel branch above) —
|
|
2917
|
+
// otherwise prepareTagExtractionPayload/getRcsPrimaryTagExtractionText send the raw "{{1}}"
|
|
2918
|
+
// token as template text, which the extract-tags API doesn't recognize, so the
|
|
2919
|
+
// synthetic-tag fallback names the row "1" instead of the real tag.
|
|
2881
2920
|
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
2882
2921
|
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
2883
2922
|
templateTitle,
|
|
2884
2923
|
templateDesc,
|
|
2885
2924
|
rcsVarRegex,
|
|
2886
2925
|
);
|
|
2926
|
+
const rawStandaloneTitle = cardContent[0].title || '';
|
|
2927
|
+
const rawStandaloneDesc = cardContent[0].description || '';
|
|
2928
|
+
const standaloneTitleVarCount = (rawStandaloneTitle.match(rcsVarRegex) || []).length;
|
|
2929
|
+
const resolvedStandaloneTitle = resolveTemplateWithMap(rawStandaloneTitle, 0);
|
|
2930
|
+
const resolvedStandaloneDesc = resolveTemplateWithMap(rawStandaloneDesc, standaloneTitleVarCount);
|
|
2887
2931
|
rcsForTest = {
|
|
2888
2932
|
...rcsForTest,
|
|
2889
2933
|
rcsContent: {
|
|
2890
2934
|
...rcsForTest.rcsContent,
|
|
2891
2935
|
cardContent: [
|
|
2892
|
-
{
|
|
2936
|
+
{
|
|
2937
|
+
...cardContent[0],
|
|
2938
|
+
title: resolvedStandaloneTitle,
|
|
2939
|
+
description: resolvedStandaloneDesc,
|
|
2940
|
+
cardVarMapped: fullCardVarMapped,
|
|
2941
|
+
},
|
|
2893
2942
|
...cardContent.slice(1),
|
|
2894
2943
|
],
|
|
2895
2944
|
},
|
|
@@ -2905,16 +2954,23 @@ const onTitleAddVar = () => {
|
|
|
2905
2954
|
},
|
|
2906
2955
|
},
|
|
2907
2956
|
};
|
|
2908
|
-
|
|
2909
|
-
//
|
|
2910
|
-
//
|
|
2911
|
-
//
|
|
2912
|
-
//
|
|
2913
|
-
|
|
2914
|
-
|
|
2957
|
+
|
|
2958
|
+
// buildSmsFallBackContentForPayload nests registeredSenderIds under smsFallBackContent.templateConfigs
|
|
2959
|
+
// only for full mode, or for campaign mode when isDltCampaign (smsRegister === 'DLT') is true — but it
|
|
2960
|
+
// always sets the flat smsFallBackContent.registeredSenderIds whenever the fallback template has them.
|
|
2961
|
+
// Extract via both so Test & Preview's DLT sender-id filter still sees real ids (and actually filters)
|
|
2962
|
+
// even when isDltCampaign was false at payload-build time.
|
|
2963
|
+
const smsFallbackRecordForTest = rcs?.rcsContent?.smsFallBackContent;
|
|
2964
|
+
const smsFallbackTcFromPayload = smsFallbackRecordForTest?.templateConfigs;
|
|
2965
|
+
const registeredSenderIdsForTest = extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackRecordForTest);
|
|
2966
|
+
const hasRegisteredSenderIdsForTest =
|
|
2967
|
+
Array.isArray(registeredSenderIdsForTest) && registeredSenderIdsForTest.length > 0;
|
|
2968
|
+
if ((smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') || hasRegisteredSenderIdsForTest) {
|
|
2915
2969
|
out.templateConfigs = {
|
|
2916
|
-
...smsFallbackTcFromPayload,
|
|
2917
|
-
|
|
2970
|
+
...(smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object' ? smsFallbackTcFromPayload : {}),
|
|
2971
|
+
...(hasRegisteredSenderIdsForTest && { registeredSenderIds: registeredSenderIdsForTest }),
|
|
2972
|
+
// Real registered sender IDs mean filtering should happen regardless of the smsRegister heuristic.
|
|
2973
|
+
traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister) || hasRegisteredSenderIdsForTest,
|
|
2918
2974
|
};
|
|
2919
2975
|
}
|
|
2920
2976
|
return out;
|
|
@@ -3077,6 +3133,19 @@ const onTitleAddVar = () => {
|
|
|
3077
3133
|
return unfilledVar;
|
|
3078
3134
|
};
|
|
3079
3135
|
|
|
3136
|
+
// Mirror WhatsApp's isEditDoneDisabled: block Done while a CTA button's dynamic URL still
|
|
3137
|
+
// holds the unresolved {{1}} placeholder (no real personalization tag assigned yet).
|
|
3138
|
+
const hasUnresolvedDynamicCtaUrl = () =>
|
|
3139
|
+
isCarouselType
|
|
3140
|
+
? (carouselData || []).some((card) =>
|
|
3141
|
+
(card?.suggestions || []).some((suggestion) =>
|
|
3142
|
+
suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
|
|
3143
|
+
)
|
|
3144
|
+
)
|
|
3145
|
+
: suggestions.some((suggestion) =>
|
|
3146
|
+
suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
|
|
3147
|
+
);
|
|
3148
|
+
|
|
3080
3149
|
const isDisableDone = () => {
|
|
3081
3150
|
if(isEditFlow){
|
|
3082
3151
|
return false;
|
|
@@ -3118,6 +3187,9 @@ const onTitleAddVar = () => {
|
|
|
3118
3187
|
return true;
|
|
3119
3188
|
}
|
|
3120
3189
|
}
|
|
3190
|
+
if (hasUnresolvedDynamicCtaUrl()) {
|
|
3191
|
+
return true;
|
|
3192
|
+
}
|
|
3121
3193
|
if (templateDescError || templateTitleError) {
|
|
3122
3194
|
return true;
|
|
3123
3195
|
}
|
|
@@ -3162,6 +3234,9 @@ const onTitleAddVar = () => {
|
|
|
3162
3234
|
return true;
|
|
3163
3235
|
}
|
|
3164
3236
|
}
|
|
3237
|
+
if (hasUnresolvedDynamicCtaUrl()) {
|
|
3238
|
+
return true;
|
|
3239
|
+
}
|
|
3165
3240
|
if (templateTitleError || templateDescError) {
|
|
3166
3241
|
return true;
|
|
3167
3242
|
}
|
|
@@ -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,18 +316,48 @@ export function resolveSmsFallbackHydrationFromDetails(details) {
|
|
|
295
316
|
const fromNested = Array.isArray(updatedEditor)
|
|
296
317
|
? updatedEditor.join('')
|
|
297
318
|
: (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
|
|
298
|
-
const
|
|
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
|
|
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;
|
|
350
|
+
const smsFallbackTemplateName =
|
|
351
|
+
smsFallbackContent.templateName
|
|
352
|
+
|| smsFallbackContent.smsTemplateName
|
|
353
|
+
|| templateConfigsRecord.templateName
|
|
354
|
+
|| templateConfigsRecord.name
|
|
355
|
+
|| '';
|
|
305
356
|
const hasFallbackPayload =
|
|
306
357
|
smsFallbackContent
|
|
307
358
|
&& Object.keys(smsFallbackContent).length > 0
|
|
308
359
|
&& (
|
|
309
|
-
!!
|
|
360
|
+
!!smsFallbackTemplateName
|
|
310
361
|
|| !!fallbackMessage
|
|
311
362
|
|| hasVarMapped
|
|
312
363
|
);
|
|
@@ -325,7 +376,7 @@ export function resolveSmsFallbackHydrationFromDetails(details) {
|
|
|
325
376
|
: (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
|
|
326
377
|
const registeredSenderIds = extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
|
|
327
378
|
const nextSmsState = {
|
|
328
|
-
templateName:
|
|
379
|
+
templateName: smsFallbackTemplateName,
|
|
329
380
|
content: fallbackMessage,
|
|
330
381
|
templateContent: fallbackMessage,
|
|
331
382
|
unicodeValidity: unicodeFromApi,
|