@capillarytech/creatives-library 8.0.329 → 8.0.330-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/constants/unified.js +4 -0
- package/package.json +1 -1
- package/utils/commonUtils.js +19 -1
- package/utils/templateVarUtils.js +35 -6
- package/utils/tests/tagValidations.test.js +20 -0
- package/utils/tests/templateVarUtils.test.js +44 -0
- package/v2Components/CapActionButton/constants.js +7 -0
- package/v2Components/CapActionButton/index.js +167 -109
- package/v2Components/CapActionButton/index.scss +157 -6
- package/v2Components/CapActionButton/messages.js +19 -3
- package/v2Components/CapActionButton/tests/index.test.js +41 -17
- package/v2Components/CapTagList/index.js +28 -23
- package/v2Components/CapTagList/style.scss +29 -0
- package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
- package/v2Components/CapTagListWithInput/index.js +4 -0
- package/v2Components/CapWhatsappCTA/index.js +2 -0
- package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +1 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +323 -77
- package/v2Components/CommonTestAndPreview/index.js +49 -57
- package/v2Components/CommonTestAndPreview/messages.js +8 -0
- package/v2Components/CommonTestAndPreview/reducer.js +3 -1
- package/v2Components/CommonTestAndPreview/sagas.js +2 -1
- package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
- package/v2Components/FormBuilder/index.js +1 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +14 -3
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +16 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +143 -28
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +5 -0
- package/v2Components/mockdata.js +1 -0
- package/v2Containers/BeeEditor/index.js +19 -1
- package/v2Containers/CreativesContainer/SlideBoxContent.js +28 -1
- package/v2Containers/CreativesContainer/index.js +9 -3
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -0
- package/v2Containers/Email/index.js +78 -39
- package/v2Containers/Email/reducer.js +2 -2
- package/v2Containers/Email/sagas.js +3 -1
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -2
- package/v2Containers/Email/tests/sagas.test.js +230 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +6 -1
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -1
- package/v2Containers/EmailWrapper/index.js +4 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +2 -0
- package/v2Containers/MobilePush/Edit/index.js +2 -0
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/constants.js +85 -7
- package/v2Containers/Rcs/index.js +1592 -156
- package/v2Containers/Rcs/index.js.rej +1336 -0
- package/v2Containers/Rcs/index.scss +191 -0
- package/v2Containers/Rcs/index.scss.rej +74 -0
- package/v2Containers/Rcs/messages.js +28 -2
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +20 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +69178 -117691
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
- package/v2Containers/Rcs/tests/index.test.js +132 -94
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +67 -0
- package/v2Containers/Rcs/tests/utils.test.js +276 -38
- package/v2Containers/Rcs/utils.js +130 -7
- package/v2Containers/Sms/Edit/index.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +27 -0
- package/v2Containers/SmsTrai/Edit/messages.js +5 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/TagList/index.js +73 -20
- package/v2Containers/TagList/messages.js +4 -0
- package/v2Containers/TagList/tests/TagList.test.js +124 -20
- package/v2Containers/TagList/tests/mockdata.js +17 -0
- package/v2Containers/Templates/_templates.scss +99 -0
- package/v2Containers/Templates/index.js +29 -14
- package/v2Containers/Viber/index.js +3 -0
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
- package/v2Containers/WebPush/Create/index.js +10 -2
- package/v2Containers/Whatsapp/index.js +5 -0
- package/v2Containers/Zalo/index.js +2 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getTemplateStatusType,
|
|
8
8
|
normalizeCardVarMapped,
|
|
9
9
|
coalesceCardVarMappedToTemplate,
|
|
10
|
+
getRcsSemanticVarNamesSpanningTitleAndDesc,
|
|
10
11
|
resolveCardVarMappedSlotValue,
|
|
11
12
|
isRcsTextOnlyCardMediaType,
|
|
12
13
|
mapRcsCardContentForConsumerWithResolvedTags,
|
|
@@ -40,38 +41,26 @@ describe('RCS utils - renderRcsSuggestionsPreview', () => {
|
|
|
40
41
|
},
|
|
41
42
|
};
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
const renderWithSuggestion = (buttonType, text) => {
|
|
44
45
|
const template = JSON.parse(JSON.stringify(templateBase));
|
|
45
46
|
template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
|
|
46
|
-
{ type:
|
|
47
|
+
{ type: buttonType, text },
|
|
47
48
|
];
|
|
48
49
|
render(getRCSContent(template));
|
|
49
|
-
|
|
50
|
-
const labels = document.querySelectorAll('.rcs-cta-preview');
|
|
51
|
-
expect(labels.length).toBe(1);
|
|
52
|
-
expect(labels[0].textContent).toContain('Reply');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('renders CTA suggestion with launch icon', () => {
|
|
56
|
-
const template = JSON.parse(JSON.stringify(templateBase));
|
|
57
|
-
template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
|
|
58
|
-
{ type: RCS_BUTTON_TYPES.CTA, text: 'Visit' },
|
|
59
|
-
];
|
|
60
|
-
render(getRCSContent(template));
|
|
61
|
-
const labels = document.querySelectorAll('.rcs-cta-preview');
|
|
62
|
-
expect(labels.length).toBe(1);
|
|
63
|
-
expect(labels[0].textContent).toContain('Visit');
|
|
64
|
-
});
|
|
50
|
+
};
|
|
65
51
|
|
|
66
|
-
it(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
52
|
+
it.each([
|
|
53
|
+
[RCS_BUTTON_TYPES.QUICK_REPLY, 'Reply', true],
|
|
54
|
+
[RCS_BUTTON_TYPES.CTA, 'Visit', false],
|
|
55
|
+
[RCS_BUTTON_TYPES.PHONE_NUMBER, 'Call', false],
|
|
56
|
+
])('renders %s suggestion with expected label', (buttonType, text, expectDivider) => {
|
|
57
|
+
renderWithSuggestion(buttonType, text);
|
|
58
|
+
if (expectDivider) {
|
|
59
|
+
expect(document.querySelectorAll('.whatsapp-divider').length).toBeGreaterThan(0);
|
|
60
|
+
}
|
|
72
61
|
const labels = document.querySelectorAll('.rcs-cta-preview');
|
|
73
62
|
expect(labels.length).toBe(1);
|
|
74
|
-
expect(labels[0].textContent).toContain(
|
|
63
|
+
expect(labels[0].textContent).toContain(text);
|
|
75
64
|
});
|
|
76
65
|
|
|
77
66
|
it('renders only divider for unknown suggestion type', () => {
|
|
@@ -96,16 +85,17 @@ describe('RCS utils - renderRcsSuggestionsPreview', () => {
|
|
|
96
85
|
|
|
97
86
|
describe('RCS utils', () => {
|
|
98
87
|
describe('getRCSContent', () => {
|
|
88
|
+
/** Single place for snapshot serialization — keeps test bodies focused on template setup. */
|
|
89
|
+
const expectGetRCSContentSnapshot = (template) => {
|
|
90
|
+
expect(renderer.create(getRCSContent(template)).toJSON()).toMatchSnapshot();
|
|
91
|
+
};
|
|
92
|
+
|
|
99
93
|
it('renders RCS content with image and CTA suggestion', () => {
|
|
100
|
-
|
|
101
|
-
const tree = renderer.create(getRCSContent(template)).toJSON();
|
|
102
|
-
expect(tree).toMatchSnapshot();
|
|
94
|
+
expectGetRCSContentSnapshot(mockData.editData1.templateDetails);
|
|
103
95
|
});
|
|
104
96
|
|
|
105
97
|
it('renders RCS content with no media', () => {
|
|
106
|
-
|
|
107
|
-
const tree = renderer.create(getRCSContent(template)).toJSON();
|
|
108
|
-
expect(tree).toMatchSnapshot();
|
|
98
|
+
expectGetRCSContentSnapshot(mockData.editData3.templateDetails);
|
|
109
99
|
});
|
|
110
100
|
|
|
111
101
|
it('renders RCS content with multiple suggestion types', () => {
|
|
@@ -115,19 +105,212 @@ describe('RCS utils', () => {
|
|
|
115
105
|
{ type: RCS_BUTTON_TYPES.CTA, text: 'URL Action' },
|
|
116
106
|
{ type: RCS_BUTTON_TYPES.PHONE_NUMBER, text: 'Call Now' },
|
|
117
107
|
];
|
|
118
|
-
|
|
119
|
-
expect(tree).toMatchSnapshot();
|
|
108
|
+
expectGetRCSContentSnapshot(template);
|
|
120
109
|
});
|
|
121
110
|
|
|
122
111
|
it('renders RCS content with missing cardContent', () => {
|
|
123
|
-
|
|
124
|
-
const tree = renderer.create(getRCSContent(template)).toJSON();
|
|
125
|
-
expect(tree).toMatchSnapshot();
|
|
112
|
+
expectGetRCSContentSnapshot({ versions: { base: { content: { RCS: { rcsContent: {} } } } } });
|
|
126
113
|
});
|
|
127
114
|
|
|
128
115
|
it('renders RCS content with empty template', () => {
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
expectGetRCSContentSnapshot({});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders RCS carousel listing preview when cardType is carousel', () => {
|
|
120
|
+
const template = JSON.parse(JSON.stringify(mockData.editData1.templateDetails));
|
|
121
|
+
const rcsContent = template.versions.base.content.RCS.rcsContent;
|
|
122
|
+
rcsContent.cardType = 'CAROUSEL';
|
|
123
|
+
rcsContent.cardContent = [
|
|
124
|
+
{
|
|
125
|
+
title: 'Card1',
|
|
126
|
+
description: 'Desc1',
|
|
127
|
+
media: { mediaUrl: 'https://example.com/1.png' },
|
|
128
|
+
suggestions: [],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
title: 'Card2',
|
|
132
|
+
description: 'Desc2',
|
|
133
|
+
media: { mediaUrl: 'https://example.com/2.png' },
|
|
134
|
+
suggestions: [],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
title: 'Card3',
|
|
138
|
+
description: 'Desc3',
|
|
139
|
+
media: { mediaUrl: 'https://example.com/3.png' },
|
|
140
|
+
suggestions: [],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
title: 'Card4',
|
|
144
|
+
description: 'Desc4',
|
|
145
|
+
media: { mediaUrl: 'https://example.com/4.png' },
|
|
146
|
+
suggestions: [],
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
render(getRCSContent(template));
|
|
151
|
+
// "Peek" shows max 3
|
|
152
|
+
expect(document.querySelectorAll('.whatsapp-carousel-container').length).toBe(3);
|
|
153
|
+
expect(document.querySelectorAll('.scroll-container').length).toBeGreaterThan(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const carouselTemplate = (cardContent) => ({
|
|
157
|
+
versions: {
|
|
158
|
+
base: {
|
|
159
|
+
content: {
|
|
160
|
+
[RCS]: {
|
|
161
|
+
rcsContent: {
|
|
162
|
+
cardType: 'carousel',
|
|
163
|
+
cardContent,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const carouselSecondCardFiller = {
|
|
172
|
+
title: 'B',
|
|
173
|
+
description: 'D',
|
|
174
|
+
media: { mediaUrl: 'https://example.com/b.png' },
|
|
175
|
+
suggestions: [],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
it('carousel: video card with thumbnailUrl uses thumbnail (not placeholder)', () => {
|
|
179
|
+
const template = carouselTemplate([
|
|
180
|
+
{
|
|
181
|
+
title: 'Vid',
|
|
182
|
+
description: 'With thumb',
|
|
183
|
+
media: {
|
|
184
|
+
mediaType: 'VIDEO',
|
|
185
|
+
mediaUrl: 'https://example.com/video.mp4',
|
|
186
|
+
thumbnailUrl: 'https://example.com/poster.jpg',
|
|
187
|
+
},
|
|
188
|
+
suggestions: [],
|
|
189
|
+
},
|
|
190
|
+
carouselSecondCardFiller,
|
|
191
|
+
]);
|
|
192
|
+
render(getRCSContent(template));
|
|
193
|
+
expect(document.querySelector('.rcs-video-preview-placeholder')).toBeFalsy();
|
|
194
|
+
const firstImg = document.querySelector('.whatsapp-carousel-card .whatsapp-image');
|
|
195
|
+
expect(firstImg).toBeTruthy();
|
|
196
|
+
expect(firstImg.getAttribute('src')).toBe('https://example.com/poster.jpg');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('carousel: shows video preview placeholder when VIDEO has mediaUrl but no thumbnail (no img src to mp4)', () => {
|
|
200
|
+
const template = carouselTemplate([
|
|
201
|
+
{
|
|
202
|
+
title: 'Clip',
|
|
203
|
+
description: 'No thumb',
|
|
204
|
+
media: {
|
|
205
|
+
mediaType: 'VIDEO',
|
|
206
|
+
mediaUrl: 'https://example.com/video.mp4',
|
|
207
|
+
},
|
|
208
|
+
suggestions: [],
|
|
209
|
+
},
|
|
210
|
+
carouselSecondCardFiller,
|
|
211
|
+
]);
|
|
212
|
+
render(getRCSContent(template));
|
|
213
|
+
expect(document.querySelector('.rcs-video-preview-placeholder')).toBeTruthy();
|
|
214
|
+
expect(document.querySelector('.rcs-video-preview-label')?.textContent).toContain('Video preview');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('carousel: uses whatsapp-message-without-media when card has no image/video URL', () => {
|
|
218
|
+
const template = carouselTemplate([
|
|
219
|
+
{
|
|
220
|
+
title: 'Text only',
|
|
221
|
+
description: 'No media',
|
|
222
|
+
media: {},
|
|
223
|
+
suggestions: [],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
title: 'Second',
|
|
227
|
+
description: '',
|
|
228
|
+
suggestions: [],
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
render(getRCSContent(template));
|
|
232
|
+
const withoutMedia = document.querySelectorAll('.whatsapp-message-without-media');
|
|
233
|
+
expect(withoutMedia.length).toBeGreaterThan(0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('carousel: body includes description with newline when description is set', () => {
|
|
237
|
+
const template = carouselTemplate([
|
|
238
|
+
{
|
|
239
|
+
title: 'Head',
|
|
240
|
+
description: 'Tail line',
|
|
241
|
+
media: { mediaUrl: 'https://example.com/x.png' },
|
|
242
|
+
suggestions: [],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
title: 'Y',
|
|
246
|
+
description: 'Z',
|
|
247
|
+
media: { mediaUrl: 'https://example.com/y.png' },
|
|
248
|
+
suggestions: [],
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
render(getRCSContent(template));
|
|
252
|
+
const bodies = document.querySelectorAll('.whatsapp-carousel-body');
|
|
253
|
+
expect(bodies[0].textContent).toContain('Head');
|
|
254
|
+
expect(bodies[0].textContent).toContain('Tail line');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('carousel: body handles missing title and empty description', () => {
|
|
258
|
+
const template = carouselTemplate([
|
|
259
|
+
{
|
|
260
|
+
description: '',
|
|
261
|
+
media: { mediaUrl: 'https://example.com/x.png' },
|
|
262
|
+
suggestions: [],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
title: 'Only title',
|
|
266
|
+
media: { mediaUrl: 'https://example.com/y.png' },
|
|
267
|
+
suggestions: [],
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
render(getRCSContent(template));
|
|
271
|
+
const bodies = document.querySelectorAll('.whatsapp-carousel-body');
|
|
272
|
+
expect(bodies[0].textContent.trim()).toBe('');
|
|
273
|
+
expect(bodies[1].textContent).toContain('Only title');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('carousel: renders per-card suggestions via renderRcsSuggestionsPreview', () => {
|
|
277
|
+
const template = carouselTemplate([
|
|
278
|
+
{
|
|
279
|
+
title: 'C1',
|
|
280
|
+
description: 'D1',
|
|
281
|
+
media: { mediaUrl: 'https://example.com/1.png' },
|
|
282
|
+
suggestions: [{ type: RCS_BUTTON_TYPES.CTA, text: 'Go' }],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
title: 'C2',
|
|
286
|
+
description: 'D2',
|
|
287
|
+
media: { mediaUrl: 'https://example.com/2.png' },
|
|
288
|
+
suggestions: [],
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
render(getRCSContent(template));
|
|
292
|
+
expect(document.querySelectorAll('.rcs-cta-preview').length).toBeGreaterThanOrEqual(1);
|
|
293
|
+
expect(Array.from(document.querySelectorAll('.rcs-cta-preview')).some((el) => el.textContent.includes('Go'))).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('carousel: does not render suggestion block when suggestions missing or not an array', () => {
|
|
297
|
+
const template = carouselTemplate([
|
|
298
|
+
{
|
|
299
|
+
title: 'A',
|
|
300
|
+
description: 'B',
|
|
301
|
+
media: { mediaUrl: 'https://example.com/a.png' },
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
title: 'C',
|
|
305
|
+
description: 'D',
|
|
306
|
+
media: { mediaUrl: 'https://example.com/c.png' },
|
|
307
|
+
suggestions: null,
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
render(getRCSContent(template));
|
|
311
|
+
const firstCard = document.querySelectorAll('.whatsapp-carousel-card')[0];
|
|
312
|
+
const dividersInFirst = firstCard.querySelectorAll('.whatsapp-divider');
|
|
313
|
+
expect(dividersInFirst.length).toBe(0);
|
|
131
314
|
});
|
|
132
315
|
});
|
|
133
316
|
|
|
@@ -215,7 +398,39 @@ describe('RCS utils', () => {
|
|
|
215
398
|
});
|
|
216
399
|
});
|
|
217
400
|
|
|
401
|
+
describe('getRcsSemanticVarNamesSpanningTitleAndDesc', () => {
|
|
402
|
+
it('returns names that appear in both title and description', () => {
|
|
403
|
+
const set = getRcsSemanticVarNamesSpanningTitleAndDesc(
|
|
404
|
+
'Hello {{adv}}',
|
|
405
|
+
'Body {{adv}} x {{other}}',
|
|
406
|
+
rcsVarRegex,
|
|
407
|
+
);
|
|
408
|
+
expect(Array.from(set).sort()).toEqual(['adv']);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('is empty when the same name is only repeated in the title', () => {
|
|
412
|
+
const set = getRcsSemanticVarNamesSpanningTitleAndDesc(
|
|
413
|
+
'{{user_name}} and {{user_name}}',
|
|
414
|
+
'Hi',
|
|
415
|
+
rcsVarRegex,
|
|
416
|
+
);
|
|
417
|
+
expect(set.size).toBe(0);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
218
421
|
describe('coalesceCardVarMappedToTemplate', () => {
|
|
422
|
+
it('does not copy a shared semantic value into the second slot when the tag spans title and description', () => {
|
|
423
|
+
const out = coalesceCardVarMappedToTemplate(
|
|
424
|
+
{ adv: 'shared' },
|
|
425
|
+
'T {{adv}}',
|
|
426
|
+
'D {{adv}}',
|
|
427
|
+
rcsVarRegex,
|
|
428
|
+
);
|
|
429
|
+
expect(out[1]).toBe('shared');
|
|
430
|
+
expect(out[2]).toBe('');
|
|
431
|
+
expect(out.adv).toBe('shared');
|
|
432
|
+
});
|
|
433
|
+
|
|
219
434
|
it('maps slot 1 to first token name from title', () => {
|
|
220
435
|
expect(
|
|
221
436
|
coalesceCardVarMappedToTemplate(
|
|
@@ -332,6 +547,24 @@ describe('RCS utils', () => {
|
|
|
332
547
|
),
|
|
333
548
|
).toBe('{{loyalty_points}}');
|
|
334
549
|
});
|
|
550
|
+
|
|
551
|
+
it('omitSemanticFallback: duplicate title+desc tag does not read shared semantic for either slot', () => {
|
|
552
|
+
expect(
|
|
553
|
+
resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: '', 2: '' }, 'adv', 0, false, true),
|
|
554
|
+
).toBe('');
|
|
555
|
+
expect(
|
|
556
|
+
resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: 'A', 2: 'B' }, 'adv', 0, false, true),
|
|
557
|
+
).toBe('A');
|
|
558
|
+
expect(
|
|
559
|
+
resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: 'A', 2: 'B' }, 'adv', 1, false, true),
|
|
560
|
+
).toBe('B');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('omitSemanticFallback: cleared semantic does not force empty when numeric slot has a value', () => {
|
|
564
|
+
expect(
|
|
565
|
+
resolveCardVarMappedSlotValue({ adv: '', 1: 'first', 2: 'second' }, 'adv', 1, false, true),
|
|
566
|
+
).toBe('second');
|
|
567
|
+
});
|
|
335
568
|
});
|
|
336
569
|
|
|
337
570
|
describe('mapRcsCardContentForConsumerWithResolvedTags', () => {
|
|
@@ -374,6 +607,11 @@ describe('RCS utils', () => {
|
|
|
374
607
|
expect(out[0].title).toBe('Hi {{customer_name}}');
|
|
375
608
|
expect(out[0].description).toBe('Pts {{loyalty_points}}');
|
|
376
609
|
});
|
|
610
|
+
|
|
611
|
+
it('passes through null or non-object items in the card array unchanged', () => {
|
|
612
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags([null, 'string', 42], {}, false);
|
|
613
|
+
expect(out).toEqual([null, 'string', 42]);
|
|
614
|
+
});
|
|
377
615
|
});
|
|
378
616
|
|
|
379
617
|
describe('isRcsTextOnlyCardMediaType', () => {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
COMBINED_SMS_TEMPLATE_VAR_REGEX,
|
|
23
23
|
isAnyTemplateVarToken,
|
|
24
24
|
} from '../../utils/templateVarUtils';
|
|
25
|
+
import { pickRcsCardVarMappedEntries } from './rcsLibraryHydrationUtils';
|
|
25
26
|
|
|
26
27
|
export const getRcsStatusType = (status) => {
|
|
27
28
|
switch (status) {
|
|
@@ -49,6 +50,13 @@ export const getTemplateStatusType = (templateStatus) => {
|
|
|
49
50
|
}
|
|
50
51
|
};
|
|
51
52
|
|
|
53
|
+
/** Localized label for a carousel video thumbnail size (width × height in px). */
|
|
54
|
+
export const formatRcsCarouselVideoThumbnailLabel = (formatMessage, dimensionEntry) => {
|
|
55
|
+
if (!dimensionEntry) return '';
|
|
56
|
+
const { width, height } = dimensionEntry;
|
|
57
|
+
return formatMessage(messages.rcsCarouselVideoThumbnailLabel, { width, height });
|
|
58
|
+
};
|
|
59
|
+
|
|
52
60
|
/**
|
|
53
61
|
* Global RegExp matching `{{numericVarName}}` in RCS template strings.
|
|
54
62
|
* `numericVarName` is escaped for regex metacharacters.
|
|
@@ -130,6 +138,27 @@ export function normalizeCardVarMapped(rawCardVarMapped, orderedTagNames) {
|
|
|
130
138
|
return normalizedMap;
|
|
131
139
|
}
|
|
132
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Semantic names that appear in both title and description (e.g. `{{adv}}` in header and body).
|
|
143
|
+
* Those slots must not share one semantic `cardVarMapped` key — otherwise VarSegment inputs mirror.
|
|
144
|
+
*/
|
|
145
|
+
export function getRcsSemanticVarNamesSpanningTitleAndDesc(
|
|
146
|
+
templateTitle,
|
|
147
|
+
templateDesc,
|
|
148
|
+
rcsVarRegex,
|
|
149
|
+
) {
|
|
150
|
+
const getVarNameFromToken = (token = '') => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
151
|
+
const titleTokens = templateTitle?.match(rcsVarRegex) ?? [];
|
|
152
|
+
const descTokens = templateDesc?.match(rcsVarRegex) ?? [];
|
|
153
|
+
const titleNames = new Set(titleTokens.map(getVarNameFromToken).filter(Boolean));
|
|
154
|
+
const descNames = new Set(descTokens.map(getVarNameFromToken).filter(Boolean));
|
|
155
|
+
const spanning = new Set();
|
|
156
|
+
titleNames.forEach((n) => {
|
|
157
|
+
if (descNames.has(n)) spanning.add(n);
|
|
158
|
+
});
|
|
159
|
+
return spanning;
|
|
160
|
+
}
|
|
161
|
+
|
|
133
162
|
/**
|
|
134
163
|
* Rebuild `cardVarMapped` so keys match the current title/description tokens (title tokens first,
|
|
135
164
|
* then description), in order. Values are taken from the matching key, else from legacy slot
|
|
@@ -140,9 +169,13 @@ export function coalesceCardVarMappedToTemplate(
|
|
|
140
169
|
templateTitle,
|
|
141
170
|
templateDesc,
|
|
142
171
|
rcsVarRegex,
|
|
172
|
+
rcsVarRegexParam,
|
|
143
173
|
) {
|
|
144
174
|
const getVarNameFromToken = (token = '') => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
145
175
|
const templateVarTokens = [
|
|
176
|
+
...(templateTitle?.match(rcsVarRegexParam) ?? []),
|
|
177
|
+
rcsVarRegexParam,
|
|
178
|
+
...(templateDesc?.match(rcsVarRegexParam) ?? []),
|
|
146
179
|
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
147
180
|
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
148
181
|
];
|
|
@@ -151,15 +184,25 @@ export function coalesceCardVarMappedToTemplate(
|
|
|
151
184
|
if (!templateVarTokens.length) {
|
|
152
185
|
return { ...lookupSourceMap };
|
|
153
186
|
}
|
|
187
|
+
const semanticNamesSpanningTitleAndDesc = getRcsSemanticVarNamesSpanningTitleAndDesc(
|
|
188
|
+
templateTitle,
|
|
189
|
+
templateDesc,
|
|
190
|
+
rcsVarRegex,
|
|
191
|
+
);
|
|
154
192
|
const coalescedMap = { ...lookupSourceMap };
|
|
155
193
|
const seenSemanticVarNames = new Set();
|
|
156
194
|
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
157
195
|
const semanticVarName = getVarNameFromToken(token);
|
|
158
196
|
if (!semanticVarName) return;
|
|
159
197
|
const numericSlotKey = String(slotIndexZeroBased + 1);
|
|
198
|
+
const isRepeatOfSemanticName = seenSemanticVarNames.has(semanticVarName);
|
|
199
|
+
const skipSharedSemanticLookup =
|
|
200
|
+
isRepeatOfSemanticName && semanticNamesSpanningTitleAndDesc.has(semanticVarName);
|
|
160
201
|
let valueFromSource = lookupSourceMap[numericSlotKey];
|
|
161
202
|
if (valueFromSource === undefined || valueFromSource === null) {
|
|
162
|
-
|
|
203
|
+
if (!skipSharedSemanticLookup) {
|
|
204
|
+
valueFromSource = lookupSourceMap[semanticVarName];
|
|
205
|
+
}
|
|
163
206
|
}
|
|
164
207
|
if (valueFromSource === undefined || valueFromSource === null) {
|
|
165
208
|
valueFromSource = lookupSourceMap[String(slotIndexZeroBased + 1)];
|
|
@@ -189,12 +232,17 @@ export function coalesceCardVarMappedToTemplate(
|
|
|
189
232
|
* When a numeric slot is present but only whitespace / empty (common after hydration), do not
|
|
190
233
|
* treat it as authoritative — fall through to the semantic key so preview and payload match the
|
|
191
234
|
* tag the user selected (e.g. `1: ''` but `promotion_points: '{{newTag}}'`).
|
|
235
|
+
*
|
|
236
|
+
* @param {boolean} [omitSemanticFallback=false] When true, do not read `varName` on the map (and do
|
|
237
|
+
* not apply the early `semanticEmpty` short-circuit). Use when the same semantic name appears in
|
|
238
|
+
* both title and description so each global slot stays independent.
|
|
192
239
|
*/
|
|
193
240
|
export function resolveCardVarMappedSlotValue(
|
|
194
241
|
cardVarMapped,
|
|
195
242
|
varName,
|
|
196
243
|
globalSlotIndexZeroBased,
|
|
197
244
|
isLibraryMode = false,
|
|
245
|
+
omitSemanticFallback = false,
|
|
198
246
|
) {
|
|
199
247
|
const varMap = cardVarMapped ?? {};
|
|
200
248
|
const slotKey = String(globalSlotIndexZeroBased + 1);
|
|
@@ -205,11 +253,12 @@ export function resolveCardVarMappedSlotValue(
|
|
|
205
253
|
Object.prototype.hasOwnProperty.call(varMap, slotKey)
|
|
206
254
|
&& String(varMap[slotKey] ?? '').trim() !== '';
|
|
207
255
|
|
|
208
|
-
if (semanticEmpty && !(isLibraryMode && slotNonEmpty)) {
|
|
256
|
+
if (semanticEmpty && !(isLibraryMode && slotNonEmpty) && !omitSemanticFallback) {
|
|
209
257
|
return '';
|
|
210
258
|
}
|
|
211
259
|
let numericSlotValue = '';
|
|
212
260
|
if (Object.prototype.hasOwnProperty.call(varMap, slotKey)) {
|
|
261
|
+
/** Text-only RCS card: editor shows a single "Text message" field (description); title row is hidden. */
|
|
213
262
|
numericSlotValue = String(varMap[slotKey] ?? '');
|
|
214
263
|
} else if (Object.prototype.hasOwnProperty.call(varMap, globalSlotIndexZeroBased + 1)) {
|
|
215
264
|
numericSlotValue = String(varMap[globalSlotIndexZeroBased + 1] ?? '');
|
|
@@ -217,7 +266,7 @@ export function resolveCardVarMappedSlotValue(
|
|
|
217
266
|
if (numericSlotValue.trim() !== '') {
|
|
218
267
|
return numericSlotValue;
|
|
219
268
|
}
|
|
220
|
-
if (Object.prototype.hasOwnProperty.call(varMap, varName)) {
|
|
269
|
+
if (!omitSemanticFallback && Object.prototype.hasOwnProperty.call(varMap, varName)) {
|
|
221
270
|
return String(varMap[varName] ?? '');
|
|
222
271
|
}
|
|
223
272
|
return '';
|
|
@@ -247,6 +296,9 @@ export function resolveRcsCardPreviewStrings(
|
|
|
247
296
|
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
248
297
|
const getVarNameFromToken = (token = '') =>
|
|
249
298
|
token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
299
|
+
const semanticNamesSpanningTitleAndDesc = textOnlyCard
|
|
300
|
+
? new Set()
|
|
301
|
+
: getRcsSemanticVarNamesSpanningTitleAndDesc(title, description, rcsVarRegex);
|
|
250
302
|
|
|
251
303
|
const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
|
|
252
304
|
if (!str) return '';
|
|
@@ -258,11 +310,13 @@ export function resolveRcsCardPreviewStrings(
|
|
|
258
310
|
const key = getVarNameFromToken(elem);
|
|
259
311
|
const globalSlot = slotOffset + varOrdinal;
|
|
260
312
|
varOrdinal += 1;
|
|
313
|
+
const omitSemantic = semanticNamesSpanningTitleAndDesc.has(key);
|
|
261
314
|
const v = resolveCardVarMappedSlotValue(
|
|
262
315
|
cardVarMapped,
|
|
263
316
|
key,
|
|
264
317
|
globalSlot,
|
|
265
318
|
isLibraryMode,
|
|
319
|
+
omitSemantic,
|
|
266
320
|
);
|
|
267
321
|
if (v == null || String(v).trim() === '') return elem;
|
|
268
322
|
return String(v);
|
|
@@ -285,7 +339,8 @@ export function resolveRcsCardPreviewStrings(
|
|
|
285
339
|
/**
|
|
286
340
|
* Campaign consumer payload: replace each card's `title` / `description` with VarSegment-resolved
|
|
287
341
|
* tag strings (same rules as {@link resolveRcsCardPreviewStrings}). Root `rcsCardVarMapped` merges
|
|
288
|
-
* with per-card `cardVarMapped
|
|
342
|
+
* with per-card `cardVarMapped` for resolution; emitted `cardVarMapped` omits SMS-fallback slot keys
|
|
343
|
+
* (root/nested merged with {@link pickRcsCardVarMappedEntries} per side).
|
|
289
344
|
*/
|
|
290
345
|
export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
291
346
|
cardContentArray,
|
|
@@ -304,7 +359,9 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
|
304
359
|
card.cardVarMapped != null && typeof card.cardVarMapped === 'object'
|
|
305
360
|
? card.cardVarMapped
|
|
306
361
|
: {};
|
|
307
|
-
const
|
|
362
|
+
const rootClean = pickRcsCardVarMappedEntries(rootRecord);
|
|
363
|
+
const nestedClean = pickRcsCardVarMappedEntries(nested);
|
|
364
|
+
const merged = { ...rootClean, ...nestedClean };
|
|
308
365
|
const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
|
|
309
366
|
const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
|
|
310
367
|
card.title ?? '',
|
|
@@ -313,10 +370,12 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
|
313
370
|
isLibraryMode,
|
|
314
371
|
textOnly,
|
|
315
372
|
);
|
|
373
|
+
const { cardVarMapped: _drop, ...cardRest } = card;
|
|
316
374
|
return {
|
|
317
|
-
...
|
|
375
|
+
...cardRest,
|
|
318
376
|
title: rcsTitle,
|
|
319
377
|
description: rcsDesc,
|
|
378
|
+
...(Object.keys(nestedClean).length > 0 ? { cardVarMapped: nestedClean } : {}),
|
|
320
379
|
};
|
|
321
380
|
});
|
|
322
381
|
}
|
|
@@ -416,6 +475,7 @@ export const getRCSContent = (template) => {
|
|
|
416
475
|
content: {
|
|
417
476
|
[RCS]: {
|
|
418
477
|
rcsContent: {
|
|
478
|
+
cardType = '',
|
|
419
479
|
cardContent = [{}],
|
|
420
480
|
cardSettings = {},
|
|
421
481
|
} = {},
|
|
@@ -431,8 +491,72 @@ export const getRCSContent = (template) => {
|
|
|
431
491
|
mediaType,
|
|
432
492
|
suggestions = [],
|
|
433
493
|
} = cardContent[0];
|
|
494
|
+
const isCarousel =
|
|
495
|
+
(cardType || '').toString().toLowerCase() === 'carousel' ||
|
|
496
|
+
(cardContent || []).length > 1;
|
|
434
497
|
const isTextOnlyCard = isRcsTextOnlyCardMediaType(mediaType);
|
|
435
498
|
const mediaPreview = media?.thumbnailUrl ? media.thumbnailUrl : media.mediaUrl;
|
|
499
|
+
|
|
500
|
+
const renderCarouselListingPreview = () => {
|
|
501
|
+
const cards = Array.isArray(cardContent) ? cardContent : [];
|
|
502
|
+
if (!cards.length) return null;
|
|
503
|
+
const cardsToShow = cards.slice(0, 3); // enough to show a "peek" of next card
|
|
504
|
+
return (
|
|
505
|
+
<div className="scroll-container">
|
|
506
|
+
{cardsToShow.map((c, idx) => {
|
|
507
|
+
const m = c?.media || {};
|
|
508
|
+
const isVideo = (m?.mediaType || '').toString().toUpperCase() === RCS_MEDIA_TYPES.VIDEO;
|
|
509
|
+
const thumbUrl = m?.thumbnailUrl;
|
|
510
|
+
const mediaUrl = m?.mediaUrl;
|
|
511
|
+
// Avoid rendering an <img src="...mp4"> when a video doesn't have a thumbnail.
|
|
512
|
+
const url = thumbUrl ? thumbUrl : (isVideo ? '' : mediaUrl);
|
|
513
|
+
return (
|
|
514
|
+
<div
|
|
515
|
+
key={`rcs-carousel-listing-${idx}-${url || ''}`}
|
|
516
|
+
className="whatsapp-carousel-container"
|
|
517
|
+
role="group"
|
|
518
|
+
>
|
|
519
|
+
<div className="whatsapp-carousel-card">
|
|
520
|
+
{url && (
|
|
521
|
+
<CapImage
|
|
522
|
+
src={url}
|
|
523
|
+
className="whatsapp-image"
|
|
524
|
+
/>
|
|
525
|
+
)}
|
|
526
|
+
{!url && isVideo && (
|
|
527
|
+
<div
|
|
528
|
+
className="whatsapp-image video-preview rcs-video-preview-placeholder"
|
|
529
|
+
>
|
|
530
|
+
<CapLabel type="label9" className="rcs-video-preview-label">
|
|
531
|
+
Video preview
|
|
532
|
+
</CapLabel>
|
|
533
|
+
</div>
|
|
534
|
+
)}
|
|
535
|
+
<span
|
|
536
|
+
className={`${url ? 'whatsapp-message-with-media' : 'whatsapp-message-without-media'}`}
|
|
537
|
+
>
|
|
538
|
+
<CapLabel type="label9" className="whatsapp-carousel-body">
|
|
539
|
+
{c?.title || ''}
|
|
540
|
+
{c?.description ? `\n${c?.description}` : ''}
|
|
541
|
+
</CapLabel>
|
|
542
|
+
</span>
|
|
543
|
+
{Array.isArray(c?.suggestions) && c.suggestions.length > 0 && (
|
|
544
|
+
<>
|
|
545
|
+
{renderRcsSuggestionsPreview(c.suggestions)}
|
|
546
|
+
</>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
})}
|
|
552
|
+
</div>
|
|
553
|
+
);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
if (isCarousel) {
|
|
557
|
+
return renderCarouselListingPreview();
|
|
558
|
+
}
|
|
559
|
+
|
|
436
560
|
return (
|
|
437
561
|
<div className="cap-rcs-creatives">
|
|
438
562
|
{mediaPreview && (
|
|
@@ -457,4 +581,3 @@ export const getRCSContent = (template) => {
|
|
|
457
581
|
</div>
|
|
458
582
|
);
|
|
459
583
|
};
|
|
460
|
-
|
|
@@ -1091,6 +1091,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1091
1091
|
onPreviewContentClicked={this.props.onPreviewContentClicked}
|
|
1092
1092
|
onTestContentClicked={this.props.onTestContentClicked}
|
|
1093
1093
|
eventContextTags={this.props?.eventContextTags}
|
|
1094
|
+
waitEventContextTags={this.props?.waitEventContextTags}
|
|
1094
1095
|
messageDetails={this.props?.messageDetails}
|
|
1095
1096
|
/>
|
|
1096
1097
|
</CapColumn>
|
|
@@ -1131,6 +1132,7 @@ Edit.propTypes = {
|
|
|
1131
1132
|
injectedTags: PropTypes.object,
|
|
1132
1133
|
selectedOfferDetails: PropTypes.array,
|
|
1133
1134
|
eventContextTags: PropTypes.array,
|
|
1135
|
+
waitEventContextTags: PropTypes.object,
|
|
1134
1136
|
messageDetails: PropTypes.object,
|
|
1135
1137
|
showTestAndPreviewSlidebox: PropTypes.bool,
|
|
1136
1138
|
handleTestAndPreview: PropTypes.func,
|