@capillarytech/creatives-library 9.0.29 → 9.0.30
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/constants/unified.js +29 -0
- package/global-styles.js +1 -1
- package/package.json +1 -1
- package/services/tests/api.test.js +35 -20
- package/utils/commonUtils.js +22 -1
- package/utils/rcsPayloadUtils.js +102 -0
- package/utils/templateVarUtils.js +198 -0
- package/utils/tests/rcsPayloadUtils.test.js +295 -0
- package/utils/tests/templateVarUtils.test.js +204 -0
- package/v2Components/CapActionButton/constants.js +21 -1
- package/v2Components/CapActionButton/index.js +261 -144
- package/v2Components/CapActionButton/index.scss +245 -14
- package/v2Components/CapActionButton/messages.js +32 -3
- package/v2Components/CapActionButton/tests/index.test.js +74 -19
- package/v2Components/CapImageUpload/index.js +2 -2
- package/v2Components/CapTagList/index.js +9 -0
- package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
- package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
- package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
- package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
- package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
- package/v2Components/CommonTestAndPreview/constants.js +35 -2
- package/v2Components/CommonTestAndPreview/index.js +770 -231
- package/v2Components/CommonTestAndPreview/messages.js +45 -3
- package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
- package/v2Components/CommonTestAndPreview/sagas.js +25 -6
- package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
- package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
- package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
- package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
- package/v2Components/CommonTestAndPreview/utils.js +84 -0
- package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
- package/v2Components/SmsFallback/constants.js +94 -0
- package/v2Components/SmsFallback/index.js +958 -0
- package/v2Components/SmsFallback/index.scss +266 -0
- package/v2Components/SmsFallback/messages.js +78 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
- package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
- package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
- package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
- package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
- package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
- package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +147 -32
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
- package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
- package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
- package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
- package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
- package/v2Components/VarSegmentMessageEditor/index.js +125 -0
- package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
- package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
- package/v2Containers/CreativesContainer/constants.js +11 -0
- package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
- package/v2Containers/CreativesContainer/index.js +322 -106
- package/v2Containers/CreativesContainer/index.scss +102 -1
- package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
- package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
- package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
- package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
- package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
- package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
- package/v2Containers/Rcs/carouselUtils.js +224 -0
- package/v2Containers/Rcs/components/CarouselCard.js +317 -0
- package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
- package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
- package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
- package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
- package/v2Containers/Rcs/constants.js +132 -16
- package/v2Containers/Rcs/index.js +1822 -946
- package/v2Containers/Rcs/index.scss +443 -8
- package/v2Containers/Rcs/messages.js +45 -22
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +494 -0
- package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
- package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
- package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
- package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
- package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74440 -39387
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
- package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
- package/v2Containers/Rcs/tests/index.test.js +275 -40
- package/v2Containers/Rcs/tests/mockData.js +38 -0
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +912 -0
- package/v2Containers/Rcs/tests/utils.test.js +682 -30
- package/v2Containers/Rcs/utils.js +514 -12
- package/v2Containers/Sms/Create/index.js +115 -48
- package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
- package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
- package/v2Containers/SmsTrai/Create/index.js +9 -4
- package/v2Containers/SmsTrai/Edit/constants.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +678 -169
- package/v2Containers/SmsTrai/Edit/index.scss +126 -0
- package/v2Containers/SmsTrai/Edit/messages.js +14 -4
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
- package/v2Containers/SmsWrapper/index.js +37 -8
- package/v2Containers/TagList/index.js +5 -0
- package/v2Containers/Templates/TemplatesActionBar.js +101 -0
- package/v2Containers/Templates/_templates.scss +196 -12
- package/v2Containers/Templates/actions.js +11 -0
- package/v2Containers/Templates/constants.js +2 -0
- package/v2Containers/Templates/index.js +131 -59
- package/v2Containers/Templates/sagas.js +57 -13
- package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
- package/v2Containers/Templates/tests/sagas.test.js +199 -16
- package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
- package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
- package/v2Containers/TemplatesV2/index.js +88 -25
- package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
- package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
- package/v2Containers/Whatsapp/index.js +3 -20
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {CapIcon, CapImage, CapLabel, CapDivider } from '@capillarytech/cap-ui-library';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
RCS,
|
|
5
|
+
RCS_MEDIA_TYPES,
|
|
6
|
+
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
7
|
+
REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
|
|
8
|
+
RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
|
|
9
|
+
} from './constants';
|
|
4
10
|
import './index.scss';
|
|
5
11
|
// import { formatMessage } from '../../../utils/intl';
|
|
6
12
|
import messages from './messages';
|
|
7
|
-
import {
|
|
8
|
-
|
|
13
|
+
import {
|
|
14
|
+
STATUS_OPTIONS,
|
|
15
|
+
RCS_BUTTON_TYPES,
|
|
16
|
+
RCS_STATUSES,
|
|
17
|
+
rcsVarRegex,
|
|
18
|
+
rcsVarTestRegex,
|
|
19
|
+
} from './constants';
|
|
20
|
+
import {
|
|
21
|
+
splitTemplateVarString,
|
|
22
|
+
COMBINED_SMS_TEMPLATE_VAR_REGEX,
|
|
23
|
+
isAnyTemplateVarToken,
|
|
24
|
+
} from '../../utils/templateVarUtils';
|
|
25
|
+
import {
|
|
26
|
+
pickRcsCardVarMappedEntries,
|
|
27
|
+
hasMeaningfulSmsFallbackShape,
|
|
28
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData,
|
|
29
|
+
pickFirstSmsFallbackTemplateString,
|
|
30
|
+
} from './rcsLibraryHydrationUtils';
|
|
31
|
+
import { isTraiDLTEnable } from '../../utils/common';
|
|
9
32
|
|
|
10
33
|
export const getRcsStatusType = (status) => {
|
|
11
34
|
switch (status) {
|
|
@@ -33,6 +56,430 @@ export const getTemplateStatusType = (templateStatus) => {
|
|
|
33
56
|
}
|
|
34
57
|
};
|
|
35
58
|
|
|
59
|
+
/** Localized label for a carousel video thumbnail size (width × height in px). */
|
|
60
|
+
export const formatRcsCarouselVideoThumbnailLabel = (formatMessage, dimensionEntry) => {
|
|
61
|
+
if (!dimensionEntry) return '';
|
|
62
|
+
const { width, height } = dimensionEntry;
|
|
63
|
+
return formatMessage(messages.rcsCarouselVideoThumbnailLabel, { width, height });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Global RegExp matching `{{numericVarName}}` in RCS template strings.
|
|
68
|
+
* `numericVarName` is escaped for regex metacharacters.
|
|
69
|
+
*/
|
|
70
|
+
export function buildRcsNumericMustachePlaceholderRegex(numericVarName) {
|
|
71
|
+
const escaped = String(numericVarName).replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
|
|
72
|
+
return new RegExp(`\\{\\{${escaped}\\}\\}`, 'g');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizeCardVarMapped(rawCardVarMapped, orderedTagNames) {
|
|
76
|
+
if (!rawCardVarMapped || typeof rawCardVarMapped !== 'object') return {};
|
|
77
|
+
const normalizedMap = {};
|
|
78
|
+
const templateVarNamesInOrder = Array.isArray(orderedTagNames) ? orderedTagNames : null;
|
|
79
|
+
const hasOrderedSlots =
|
|
80
|
+
Boolean(templateVarNamesInOrder?.length);
|
|
81
|
+
Object.entries(rawCardVarMapped).forEach(([entryKey, entryValue]) => {
|
|
82
|
+
const trimmedValue = entryValue == null ? '' : String(entryValue).trim();
|
|
83
|
+
const entryKeyIsNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(entryKey));
|
|
84
|
+
const mustacheInnerMatch = trimmedValue.match(/^\{\{([^}]+)\}\}$/);
|
|
85
|
+
const innerFromMustache =
|
|
86
|
+
mustacheInnerMatch?.[1] != null ? String(mustacheInnerMatch[1]).trim() : null;
|
|
87
|
+
|
|
88
|
+
if (innerFromMustache !== null && entryKeyIsNumericSlot) {
|
|
89
|
+
const slotIndexZeroBased = parseInt(String(entryKey), 10) - 1;
|
|
90
|
+
const expectedVarForSlot =
|
|
91
|
+
hasOrderedSlots
|
|
92
|
+
&& slotIndexZeroBased >= 0
|
|
93
|
+
&& slotIndexZeroBased < templateVarNamesInOrder.length
|
|
94
|
+
? templateVarNamesInOrder[slotIndexZeroBased]
|
|
95
|
+
: null;
|
|
96
|
+
const innerMatchesSlotToken =
|
|
97
|
+
expectedVarForSlot != null && innerFromMustache === expectedVarForSlot;
|
|
98
|
+
const legacyUnorderedPlaceholder = !hasOrderedSlots;
|
|
99
|
+
/* Library: slot "1" + {{user_id_b64}} when token is user_id_b64 → empty semantic. With ordered
|
|
100
|
+
* slots, only clear when inner matches that slot's template token; else keep (e.g. {{1}}+{{FirstName}}). */
|
|
101
|
+
const clearNumericSlotMustacheAsUnfilled =
|
|
102
|
+
!RCS_NUMERIC_VAR_NAME_REGEX.test(innerFromMustache)
|
|
103
|
+
&& (legacyUnorderedPlaceholder || innerMatchesSlotToken);
|
|
104
|
+
if (clearNumericSlotMustacheAsUnfilled) {
|
|
105
|
+
const outputKey = innerFromMustache;
|
|
106
|
+
const existingValue = normalizedMap[outputKey];
|
|
107
|
+
if (existingValue != null && String(existingValue).trim() !== '') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
normalizedMap[outputKey] = '';
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (RCS_NUMERIC_VAR_NAME_REGEX.test(innerFromMustache)) {
|
|
114
|
+
const outputKey = innerFromMustache;
|
|
115
|
+
const existingValue = normalizedMap[outputKey];
|
|
116
|
+
if (existingValue != null && String(existingValue).trim() !== '') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
normalizedMap[outputKey] = '';
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (innerFromMustache !== null && !entryKeyIsNumericSlot) {
|
|
125
|
+
if (innerFromMustache === String(entryKey)) {
|
|
126
|
+
const existingValue = normalizedMap[entryKey];
|
|
127
|
+
if (existingValue != null && String(existingValue).trim() !== '') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
normalizedMap[entryKey] = '';
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (entryKeyIsNumericSlot && templateVarNamesInOrder?.length) {
|
|
136
|
+
const slotIndexZeroBased = parseInt(String(entryKey), 10) - 1;
|
|
137
|
+
if (slotIndexZeroBased >= 0 && slotIndexZeroBased < templateVarNamesInOrder.length) {
|
|
138
|
+
normalizedMap[templateVarNamesInOrder[slotIndexZeroBased]] = trimmedValue;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
normalizedMap[entryKey] = trimmedValue;
|
|
143
|
+
});
|
|
144
|
+
return normalizedMap;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Semantic names that appear in both title and description (e.g. `{{adv}}` in header and body).
|
|
149
|
+
* Those slots must not share one semantic `cardVarMapped` key — otherwise VarSegment inputs mirror.
|
|
150
|
+
*/
|
|
151
|
+
export function getRcsSemanticVarNamesSpanningTitleAndDesc(
|
|
152
|
+
templateTitle,
|
|
153
|
+
templateDesc,
|
|
154
|
+
rcsVarRegex,
|
|
155
|
+
) {
|
|
156
|
+
const getVarNameFromToken = (token = '') => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
157
|
+
const titleTokens = templateTitle?.match(rcsVarRegex) ?? [];
|
|
158
|
+
const descTokens = templateDesc?.match(rcsVarRegex) ?? [];
|
|
159
|
+
const titleNames = new Set(titleTokens.map(getVarNameFromToken).filter(Boolean));
|
|
160
|
+
const descNames = new Set(descTokens.map(getVarNameFromToken).filter(Boolean));
|
|
161
|
+
const spanning = new Set();
|
|
162
|
+
titleNames.forEach((name) => {
|
|
163
|
+
if (descNames.has(name)) spanning.add(name);
|
|
164
|
+
});
|
|
165
|
+
return spanning;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Rebuild `cardVarMapped` so keys match the current title/description tokens (title tokens first,
|
|
170
|
+
* then description), in order. Values are taken from the matching key, else from legacy slot
|
|
171
|
+
* `1`, `2`, … by index. If there are no `{{...}}` tokens, returns a shallow clone of `raw`.
|
|
172
|
+
*/
|
|
173
|
+
export function coalesceCardVarMappedToTemplate(
|
|
174
|
+
sourceCardVarMap,
|
|
175
|
+
templateTitle,
|
|
176
|
+
templateDesc,
|
|
177
|
+
rcsVarRegex,
|
|
178
|
+
) {
|
|
179
|
+
const getVarNameFromToken = (token = '') => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
180
|
+
const templateVarTokens = [
|
|
181
|
+
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
182
|
+
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
183
|
+
];
|
|
184
|
+
const lookupSourceMap =
|
|
185
|
+
sourceCardVarMap != null && typeof sourceCardVarMap === 'object' ? sourceCardVarMap : {};
|
|
186
|
+
if (!templateVarTokens.length) {
|
|
187
|
+
return { ...lookupSourceMap };
|
|
188
|
+
}
|
|
189
|
+
const semanticNamesSpanningTitleAndDesc = getRcsSemanticVarNamesSpanningTitleAndDesc(
|
|
190
|
+
templateTitle,
|
|
191
|
+
templateDesc,
|
|
192
|
+
rcsVarRegex,
|
|
193
|
+
);
|
|
194
|
+
const coalescedMap = { ...lookupSourceMap };
|
|
195
|
+
const seenSemanticVarNames = new Set();
|
|
196
|
+
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
197
|
+
const semanticVarName = getVarNameFromToken(token);
|
|
198
|
+
if (!semanticVarName) return;
|
|
199
|
+
const numericSlotKey = String(slotIndexZeroBased + 1);
|
|
200
|
+
const isRepeatOfSemanticName = seenSemanticVarNames.has(semanticVarName);
|
|
201
|
+
const skipSharedSemanticLookup =
|
|
202
|
+
isRepeatOfSemanticName && semanticNamesSpanningTitleAndDesc.has(semanticVarName);
|
|
203
|
+
let valueFromSource = lookupSourceMap[numericSlotKey];
|
|
204
|
+
if (valueFromSource === undefined || valueFromSource === null) {
|
|
205
|
+
if (!skipSharedSemanticLookup) {
|
|
206
|
+
valueFromSource = lookupSourceMap[semanticVarName];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (valueFromSource === undefined || valueFromSource === null) {
|
|
210
|
+
valueFromSource = lookupSourceMap[String(slotIndexZeroBased + 1)];
|
|
211
|
+
}
|
|
212
|
+
if (valueFromSource === undefined || valueFromSource === null) {
|
|
213
|
+
valueFromSource = lookupSourceMap[slotIndexZeroBased + 1];
|
|
214
|
+
}
|
|
215
|
+
const trimmedSlotValue = valueFromSource == null ? '' : String(valueFromSource).trim();
|
|
216
|
+
coalescedMap[numericSlotKey] = trimmedSlotValue;
|
|
217
|
+
if (!seenSemanticVarNames.has(semanticVarName)) {
|
|
218
|
+
seenSemanticVarNames.add(semanticVarName);
|
|
219
|
+
coalescedMap[semanticVarName] = trimmedSlotValue;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return coalescedMap;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the personalization value for a variable slot — aligned with createPayload:
|
|
227
|
+
* per-slot numeric keys `1`, `2`, … win over legacy semantic keys when both exist (duplicate
|
|
228
|
+
* `{{name}}` in title+desc). If semantic is explicitly cleared to '', that still wins over a
|
|
229
|
+
* stale numeric value (see tests) — except in embedded library / journey mode (`isLibraryMode`).
|
|
230
|
+
*
|
|
231
|
+
* In library mode, campaign payloads often set semantic keys to '' while numeric slot `1`,`2`,…
|
|
232
|
+
* still holds the value selected in the library; prefer that so VarSegment prepopulates.
|
|
233
|
+
*
|
|
234
|
+
* When a numeric slot is present but only whitespace / empty (common after hydration), do not
|
|
235
|
+
* treat it as authoritative — fall through to the semantic key so preview and payload match the
|
|
236
|
+
* tag the user selected (e.g. `1: ''` but `promotion_points: '{{newTag}}'`).
|
|
237
|
+
*
|
|
238
|
+
* @param {boolean} [omitSemanticFallback=false] When true, do not read `varName` on the map (and do
|
|
239
|
+
* not apply the early `semanticEmpty` short-circuit). Use when the same semantic name appears in
|
|
240
|
+
* both title and description so each global slot stays independent.
|
|
241
|
+
*/
|
|
242
|
+
export function resolveCardVarMappedSlotValue(
|
|
243
|
+
cardVarMapped,
|
|
244
|
+
varName,
|
|
245
|
+
globalSlotIndexZeroBased,
|
|
246
|
+
isLibraryMode = false,
|
|
247
|
+
omitSemanticFallback = false,
|
|
248
|
+
) {
|
|
249
|
+
const varMap = cardVarMapped ?? {};
|
|
250
|
+
const slotKey = String(globalSlotIndexZeroBased + 1);
|
|
251
|
+
const semanticEmpty =
|
|
252
|
+
Object.prototype.hasOwnProperty.call(varMap, varName)
|
|
253
|
+
&& String(varMap[varName] ?? '') === '';
|
|
254
|
+
const slotNonEmpty =
|
|
255
|
+
Object.prototype.hasOwnProperty.call(varMap, slotKey)
|
|
256
|
+
&& String(varMap[slotKey] ?? '').trim() !== '';
|
|
257
|
+
|
|
258
|
+
if (semanticEmpty && !(isLibraryMode && slotNonEmpty) && !omitSemanticFallback) {
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
let numericSlotValue = '';
|
|
262
|
+
if (Object.prototype.hasOwnProperty.call(varMap, slotKey)) {
|
|
263
|
+
/** Text-only RCS card: editor shows a single "Text message" field (description); title row is hidden. */
|
|
264
|
+
numericSlotValue = String(varMap[slotKey] ?? '');
|
|
265
|
+
} else if (Object.prototype.hasOwnProperty.call(varMap, globalSlotIndexZeroBased + 1)) {
|
|
266
|
+
numericSlotValue = String(varMap[globalSlotIndexZeroBased + 1] ?? '');
|
|
267
|
+
}
|
|
268
|
+
if (numericSlotValue.trim() !== '') {
|
|
269
|
+
return numericSlotValue;
|
|
270
|
+
}
|
|
271
|
+
if (!omitSemanticFallback && Object.prototype.hasOwnProperty.call(varMap, varName)) {
|
|
272
|
+
return String(varMap[varName] ?? '');
|
|
273
|
+
}
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Text-only RCS card: editor shows a single “Text message” field (description); title row is hidden. */
|
|
278
|
+
export function isRcsTextOnlyCardMediaType(mediaType) {
|
|
279
|
+
return (
|
|
280
|
+
mediaType === RCS_MEDIA_TYPES.NONE
|
|
281
|
+
|| String(mediaType || '').toUpperCase() === RCS_MEDIA_TYPES.TEXT
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolve RCS card title/description for TemplatePreview (e.g. campaign slidebox preview).
|
|
287
|
+
* Mirrors `resolveTemplateWithMap` in the Rcs editor: title vars use global slots 0..n-1, then description.
|
|
288
|
+
* For text-only cards (`textOnlyCard`), ignore persisted `title` and resolve description from slot 0 — matches
|
|
289
|
+
* the editor where only the message body is shown.
|
|
290
|
+
*/
|
|
291
|
+
export function resolveRcsCardPreviewStrings(
|
|
292
|
+
title,
|
|
293
|
+
description,
|
|
294
|
+
cardVarMapped,
|
|
295
|
+
isLibraryMode = false,
|
|
296
|
+
textOnlyCard = false,
|
|
297
|
+
) {
|
|
298
|
+
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
299
|
+
const getVarNameFromToken = (token = '') =>
|
|
300
|
+
token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
301
|
+
const semanticNamesSpanningTitleAndDesc = textOnlyCard
|
|
302
|
+
? new Set()
|
|
303
|
+
: getRcsSemanticVarNamesSpanningTitleAndDesc(title, description, rcsVarRegex);
|
|
304
|
+
|
|
305
|
+
const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
|
|
306
|
+
if (!str) return '';
|
|
307
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
308
|
+
let varOrdinal = 0;
|
|
309
|
+
return arr
|
|
310
|
+
.map((elem) => {
|
|
311
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
312
|
+
const key = getVarNameFromToken(elem);
|
|
313
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
314
|
+
varOrdinal += 1;
|
|
315
|
+
const omitSemantic = semanticNamesSpanningTitleAndDesc.has(key);
|
|
316
|
+
const slotValue = resolveCardVarMappedSlotValue(
|
|
317
|
+
cardVarMapped,
|
|
318
|
+
key,
|
|
319
|
+
globalSlot,
|
|
320
|
+
isLibraryMode,
|
|
321
|
+
omitSemantic,
|
|
322
|
+
);
|
|
323
|
+
if (slotValue == null || String(slotValue).trim() === '') return elem;
|
|
324
|
+
return String(slotValue);
|
|
325
|
+
}
|
|
326
|
+
return elem;
|
|
327
|
+
})
|
|
328
|
+
.join('');
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const effectiveTitle = textOnlyCard ? '' : String(title || '');
|
|
332
|
+
const titleVarCount = textOnlyCard
|
|
333
|
+
? 0
|
|
334
|
+
: (effectiveTitle.match(rcsVarRegex) || []).length;
|
|
335
|
+
return {
|
|
336
|
+
rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle, 0),
|
|
337
|
+
rcsDesc: resolveTemplateWithMap(String(description || ''), titleVarCount),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Campaign consumer payload: replace each card's `title` / `description` with VarSegment-resolved
|
|
343
|
+
* tag strings (same rules as {@link resolveRcsCardPreviewStrings}). Root `rcsCardVarMapped` merges
|
|
344
|
+
* with per-card `cardVarMapped` for resolution; emitted `cardVarMapped` omits SMS-fallback slot keys
|
|
345
|
+
* (root/nested merged with {@link pickRcsCardVarMappedEntries} per side).
|
|
346
|
+
*/
|
|
347
|
+
export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
348
|
+
cardContentArray,
|
|
349
|
+
rootRcsCardVarMapped,
|
|
350
|
+
isFullMode,
|
|
351
|
+
) {
|
|
352
|
+
const rootRecord =
|
|
353
|
+
rootRcsCardVarMapped != null && typeof rootRcsCardVarMapped === 'object'
|
|
354
|
+
? rootRcsCardVarMapped
|
|
355
|
+
: {};
|
|
356
|
+
const list = Array.isArray(cardContentArray) ? cardContentArray : [];
|
|
357
|
+
const isLibraryMode = isFullMode !== true;
|
|
358
|
+
return list.map((card) => {
|
|
359
|
+
if (!card || typeof card !== 'object') return card;
|
|
360
|
+
const nested =
|
|
361
|
+
card.cardVarMapped != null && typeof card.cardVarMapped === 'object'
|
|
362
|
+
? card.cardVarMapped
|
|
363
|
+
: {};
|
|
364
|
+
const rootClean = pickRcsCardVarMappedEntries(rootRecord);
|
|
365
|
+
const nestedClean = pickRcsCardVarMappedEntries(nested);
|
|
366
|
+
const merged = { ...rootClean, ...nestedClean };
|
|
367
|
+
const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
|
|
368
|
+
const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
|
|
369
|
+
card.title ?? '',
|
|
370
|
+
card.description ?? '',
|
|
371
|
+
merged,
|
|
372
|
+
isLibraryMode,
|
|
373
|
+
textOnly,
|
|
374
|
+
);
|
|
375
|
+
const { cardVarMapped: _drop, ...cardRest } = card;
|
|
376
|
+
return {
|
|
377
|
+
...cardRest,
|
|
378
|
+
title: rcsTitle,
|
|
379
|
+
description: rcsDesc,
|
|
380
|
+
...(Object.keys(nestedClean).length > 0 ? { cardVarMapped: nestedClean } : {}),
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Before save: strip only legacy numeric self-placeholders (`{{1}}`, `{{2}}`, …) mistakenly stored as
|
|
387
|
+
* slot values. Preserve semantic tokens like `{{FirstName}}` from TagList — those are valid mappings.
|
|
388
|
+
*/
|
|
389
|
+
export function sanitizeCardVarMappedValue(val) {
|
|
390
|
+
if (val == null) return '';
|
|
391
|
+
const trimmedDisplayString = String(val).trim();
|
|
392
|
+
if (/^\{\{\d+\}\}$/.test(trimmedDisplayString)) return '';
|
|
393
|
+
return String(val);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Same completion rule as SmsTraiEdit RCS fallback — used by `isDisableDone` from
|
|
398
|
+
* `smsFallbackData.rcsSmsFallbackVarMapped` + template string.
|
|
399
|
+
* Every variable token (DLT {#…#} or mustache {{…}}) must have a non-empty trimmed value in the map.
|
|
400
|
+
*
|
|
401
|
+
* Slot keys are usually `${token}_${segmentIndex}` (same as VarSegmentMessageEditor). Persisted / API
|
|
402
|
+
* payloads may use `${token}_${varOrdinal}` with a 1-based occurrence index (see SmsTraiEdit init).
|
|
403
|
+
* We try segment index first, then ordinal — so e.g. template `{#var#}` (segment index 0) still matches
|
|
404
|
+
* map `{#var#}_1`.
|
|
405
|
+
*
|
|
406
|
+
* @param {string} templateText
|
|
407
|
+
* @param {Record<string, string>} [varSlotValueMap={}]
|
|
408
|
+
* @returns {boolean}
|
|
409
|
+
*/
|
|
410
|
+
export function areAllRcsSmsFallbackVarSlotsFilled(templateText, varSlotValueMap = {}) {
|
|
411
|
+
if (!templateText || typeof templateText !== 'string') return true;
|
|
412
|
+
const segments = splitTemplateVarString(templateText, COMBINED_SMS_TEMPLATE_VAR_REGEX);
|
|
413
|
+
const hasVarToken = segments.some(
|
|
414
|
+
(segment) =>
|
|
415
|
+
typeof segment === 'string'
|
|
416
|
+
&& isAnyTemplateVarToken(segment),
|
|
417
|
+
);
|
|
418
|
+
if (!hasVarToken) return true;
|
|
419
|
+
let varOrdinal = 0;
|
|
420
|
+
return segments.every((segment, segmentIndex) => {
|
|
421
|
+
if (
|
|
422
|
+
typeof segment !== 'string'
|
|
423
|
+
|| !isAnyTemplateVarToken(segment)
|
|
424
|
+
) return true;
|
|
425
|
+
varOrdinal += 1;
|
|
426
|
+
const indexKey = `${segment}_${segmentIndex}`;
|
|
427
|
+
const ordinalKey = `${segment}_${varOrdinal}`;
|
|
428
|
+
let mappedSlotValue;
|
|
429
|
+
if (Object.prototype.hasOwnProperty.call(varSlotValueMap, indexKey)) {
|
|
430
|
+
mappedSlotValue = varSlotValueMap[indexKey];
|
|
431
|
+
} else if (Object.prototype.hasOwnProperty.call(varSlotValueMap, ordinalKey)) {
|
|
432
|
+
mappedSlotValue = varSlotValueMap[ordinalKey];
|
|
433
|
+
} else {
|
|
434
|
+
mappedSlotValue = undefined;
|
|
435
|
+
}
|
|
436
|
+
if (mappedSlotValue == null) return false;
|
|
437
|
+
return String(mappedSlotValue).trim() !== '';
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** When a fallback SMS row exists, require non-empty body (trimmed) and filled var slots (DLT). */
|
|
442
|
+
export function checkSmsFallbackBlocksDone({
|
|
443
|
+
isFullMode,
|
|
444
|
+
smsRegister,
|
|
445
|
+
smsFallbackData,
|
|
446
|
+
templateData,
|
|
447
|
+
smsFallbackMergedForValidation,
|
|
448
|
+
}) {
|
|
449
|
+
// Non-DLT library: user removed SMS fallback (local null) but template still carries fallback — block Done.
|
|
450
|
+
if (
|
|
451
|
+
!isFullMode
|
|
452
|
+
&& !isTraiDLTEnable(isFullMode, smsRegister)
|
|
453
|
+
&& smsFallbackData == null
|
|
454
|
+
&& hasMeaningfulSmsFallbackShape(
|
|
455
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
456
|
+
)
|
|
457
|
+
) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
if (!smsFallbackData) return false;
|
|
461
|
+
// Full-mode (Send for approval): SMS fallback is optional. Tag-slot mapping is a display/preview
|
|
462
|
+
// concern, not a structural requirement for approval — the registered SMS template body stands on
|
|
463
|
+
// its own. Never block the Send for approval button due to missing or unfilled fallback var slots.
|
|
464
|
+
if (isFullMode) return false;
|
|
465
|
+
const merged = smsFallbackMergedForValidation;
|
|
466
|
+
const templateText = pickFirstSmsFallbackTemplateString(merged);
|
|
467
|
+
if (!templateText) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
const rawVarMap =
|
|
471
|
+
merged.rcsSmsFallbackVarMapped
|
|
472
|
+
|| merged['rcs-sms-fallback-var-mapped'];
|
|
473
|
+
const varMap =
|
|
474
|
+
rawVarMap != null && typeof rawVarMap === 'object' ? rawVarMap : {};
|
|
475
|
+
return !areAllRcsSmsFallbackVarSlotsFilled(templateText, varMap);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Done/edit-done gating: SMS fallback body over the max length blocks submit. */
|
|
479
|
+
export function isSmsFallbackContentTooLong(smsFallbackData, maxLength) {
|
|
480
|
+
return !!(smsFallbackData?.content && smsFallbackData.content.length > maxLength);
|
|
481
|
+
}
|
|
482
|
+
|
|
36
483
|
export const getRCSContent = (template) => {
|
|
37
484
|
const renderRcsSuggestionsPreview = (rcsSuggestions) => {
|
|
38
485
|
const renderArray = [];
|
|
@@ -51,7 +498,7 @@ export const getRCSContent = (template) => {
|
|
|
51
498
|
} else if (suggestion.type === RCS_BUTTON_TYPES.CTA) {
|
|
52
499
|
renderArray.push(
|
|
53
500
|
<CapLabel type="label21" className="rcs-cta-preview">
|
|
54
|
-
<CapIcon type="
|
|
501
|
+
<CapIcon type="open-in-new" size="xs" />
|
|
55
502
|
{suggestion.text}
|
|
56
503
|
</CapLabel>,
|
|
57
504
|
);
|
|
@@ -72,6 +519,7 @@ export const getRCSContent = (template) => {
|
|
|
72
519
|
content: {
|
|
73
520
|
[RCS]: {
|
|
74
521
|
rcsContent: {
|
|
522
|
+
cardType = '',
|
|
75
523
|
cardContent = [{}],
|
|
76
524
|
cardSettings = {},
|
|
77
525
|
} = {},
|
|
@@ -84,9 +532,62 @@ export const getRCSContent = (template) => {
|
|
|
84
532
|
media = {},
|
|
85
533
|
description,
|
|
86
534
|
title,
|
|
535
|
+
mediaType,
|
|
87
536
|
suggestions = [],
|
|
88
537
|
} = cardContent[0];
|
|
538
|
+
const isCarousel =
|
|
539
|
+
(cardType || '').toString().toLowerCase() === 'carousel' ||
|
|
540
|
+
(cardContent || []).length > 1;
|
|
541
|
+
const isTextOnlyCard = isRcsTextOnlyCardMediaType(mediaType);
|
|
89
542
|
const mediaPreview = media?.thumbnailUrl ? media.thumbnailUrl : media.mediaUrl;
|
|
543
|
+
|
|
544
|
+
// Listing preview shows only the FIRST card — no peek of other cards, no buttons (Figma).
|
|
545
|
+
const renderCarouselListingPreview = () => {
|
|
546
|
+
const cards = Array.isArray(cardContent) ? cardContent : [];
|
|
547
|
+
if (!cards.length) return null;
|
|
548
|
+
const card = cards[0];
|
|
549
|
+
const cardMedia = card?.media || {};
|
|
550
|
+
const isVideo = (card?.mediaType || cardMedia?.mediaType || '').toString().toUpperCase() === RCS_MEDIA_TYPES.VIDEO;
|
|
551
|
+
const thumbUrl = cardMedia?.thumbnailUrl;
|
|
552
|
+
const mediaUrl = cardMedia?.mediaUrl;
|
|
553
|
+
// Avoid rendering an <img src="...mp4"> when a video doesn't have a thumbnail.
|
|
554
|
+
const url = thumbUrl ? thumbUrl : (isVideo ? '' : mediaUrl);
|
|
555
|
+
return (
|
|
556
|
+
<div className="whatsapp-carousel-container" role="group">
|
|
557
|
+
<div className="whatsapp-carousel-card">
|
|
558
|
+
{/* Text and media render as two separate rounded cards, not one merged
|
|
559
|
+
box (Figma listing reference) — description only, no card title. */}
|
|
560
|
+
<div className="rcs-carousel-listing-text-box">
|
|
561
|
+
<CapLabel type="label9" className="whatsapp-carousel-body">
|
|
562
|
+
{card?.description || ''}
|
|
563
|
+
</CapLabel>
|
|
564
|
+
</div>
|
|
565
|
+
<div className="rcs-carousel-listing-media-box">
|
|
566
|
+
{url && (
|
|
567
|
+
<CapImage
|
|
568
|
+
src={url}
|
|
569
|
+
className="whatsapp-image"
|
|
570
|
+
/>
|
|
571
|
+
)}
|
|
572
|
+
{!url && isVideo && (
|
|
573
|
+
<div
|
|
574
|
+
className="whatsapp-image video-preview rcs-video-preview-placeholder"
|
|
575
|
+
>
|
|
576
|
+
<CapLabel type="label9" className="rcs-video-preview-label">
|
|
577
|
+
Video preview
|
|
578
|
+
</CapLabel>
|
|
579
|
+
</div>
|
|
580
|
+
)}
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
if (isCarousel) {
|
|
588
|
+
return renderCarouselListingPreview();
|
|
589
|
+
}
|
|
590
|
+
|
|
90
591
|
return (
|
|
91
592
|
<div className="cap-rcs-creatives">
|
|
92
593
|
{mediaPreview && (
|
|
@@ -95,13 +596,15 @@ export const getRCSContent = (template) => {
|
|
|
95
596
|
className="rcs-listing-image"
|
|
96
597
|
/>
|
|
97
598
|
)}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
599
|
+
{!isTextOnlyCard && (
|
|
600
|
+
<CapLabel
|
|
601
|
+
type="label19"
|
|
602
|
+
className="rcs-listing-content title"
|
|
603
|
+
fontWeight="bold"
|
|
604
|
+
>
|
|
605
|
+
{title}
|
|
606
|
+
</CapLabel>
|
|
607
|
+
)}
|
|
105
608
|
<CapLabel type="label19" className="rcs-listing-content desc">
|
|
106
609
|
{description}
|
|
107
610
|
</CapLabel>
|
|
@@ -109,4 +612,3 @@ export const getRCSContent = (template) => {
|
|
|
109
612
|
</div>
|
|
110
613
|
);
|
|
111
614
|
};
|
|
112
|
-
|