@capillarytech/creatives-library 8.0.353-alpha.3 → 8.0.353-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +17 -0
  3. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +14 -132
  4. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +169 -0
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +70 -163
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +44 -5
  7. package/v2Components/CommonTestAndPreview/constants.js +2 -0
  8. package/v2Components/CommonTestAndPreview/index.js +58 -49
  9. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +159 -0
  10. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +0 -364
  11. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +522 -0
  12. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +255 -0
  13. package/v2Components/CommonTestAndPreview/tests/constants.test.js +2 -1
  14. package/v2Components/CommonTestAndPreview/tests/index.test.js +194 -0
  15. package/v2Components/TestAndPreviewSlidebox/index.js +2 -2
  16. package/v2Containers/App/constants.js +3 -0
  17. package/v2Containers/App/tests/constants.test.js +61 -0
  18. package/v2Containers/Templates/_templates.scss +0 -77
  19. package/v2Containers/Templates/index.js +70 -77
  20. package/v2Containers/Templates/tests/webpush.test.js +375 -0
  21. package/v2Containers/Viber/constants.js +0 -19
  22. package/v2Containers/Viber/index.js +47 -714
  23. package/v2Containers/Viber/index.scss +0 -148
  24. package/v2Containers/Viber/messages.js +0 -116
  25. package/v2Containers/Viber/tests/index.test.js +0 -80
  26. package/v2Containers/WebPush/Create/index.js +91 -8
  27. package/v2Containers/WebPush/Create/index.scss +7 -0
  28. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +338 -0
  29. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +325 -0
@@ -72,7 +72,6 @@ import * as ebillActions from '../Ebill/actions';
72
72
  import * as emailActions from '../Email/actions';
73
73
  import * as lineActions from '../Line/Container/actions';
74
74
  import * as viberActions from '../Viber/actions';
75
- import { VIBER_MEDIA_TYPES } from '../Viber/constants';
76
75
  import * as facebookActions from '../Facebook/actions';
77
76
  import * as whatsappActions from '../Whatsapp/actions';
78
77
  import * as rcsActions from '../Rcs/actions';
@@ -105,6 +104,9 @@ import {
105
104
  VIBER as VIBER_CHANNEL,
106
105
  FACEBOOK as FACEBOOK_CHANNEL,
107
106
  CREATE,
107
+ EXTERNAL_URL,
108
+ URL,
109
+ SITE_URL,
108
110
  } from '../App/constants';
109
111
  import {MAX_WHATSAPP_TEMPLATES, WARNING_WHATSAPP_TEMPLATES , ACCOUNT_MAPPING_ON_CHANNEL, noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc, noApprovedWhatsappZaloTemplatesTitle, noApprovedWhatsappTemplatesDesc, zaloDescIllustration, noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc, ARCHIVE_STATUS_ACTIVE, ARCHIVE_STATUS_ARCHIVED, ARCHIVE_REFRESH_TYPE_ARCHIVE, ARCHIVE_REFRESH_TYPE_UNARCHIVE} from './constants';
110
112
  import { COPY_OF, EMBEDDED } from '../../constants/unified';
@@ -1372,11 +1374,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1372
1374
  const image = viberContent.image || {};
1373
1375
  const video = viberContent.video || {};
1374
1376
  const button = viberContent.button || {};
1375
- const messageType = viberContent.type || '';
1376
- const cardsRaw = viberContent.cards;
1377
- const cards = Array.isArray(cardsRaw) ? cardsRaw : [];
1378
- const isCarousel =
1379
- messageType === VIBER_MEDIA_TYPES.CAROUSEL || cards.length > 0;
1380
1377
 
1381
1378
  const viberPreview = {
1382
1379
  messageContent: text,
@@ -1394,39 +1391,77 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1394
1391
  };
1395
1392
  }
1396
1393
 
1397
- if (isCarousel) {
1398
- viberPreview.type = VIBER_MEDIA_TYPES.CAROUSEL;
1399
- viberPreview.cards = cards.map((card) => ({
1400
- text: card?.text || '',
1401
- mediaUrl: card?.mediaUrl || '',
1402
- buttons: (card?.buttons || []).map((btn) => ({
1403
- title: btn?.title || '',
1404
- action: btn?.action || '',
1405
- })),
1406
- }));
1407
- viberPreview.showCarouselEditorPreview = true;
1408
- }
1409
-
1410
1394
  // Extract account name
1411
1395
  const accountName = get(template, 'definition.accountName', '');
1412
1396
 
1413
1397
  // Return Viber content object (same as Viber/index.js getTemplateContent)
1414
1398
  return {
1415
1399
  viberPreviewContent: viberPreview,
1416
- accountName: accountName || '',
1417
- brandName: accountName || '',
1418
- messageContent: text,
1419
- ...(isCarousel && {
1420
- type: VIBER_MEDIA_TYPES.CAROUSEL,
1421
- cards: viberPreview.cards,
1422
- }),
1423
- ...(!isCarousel && !isEmpty(button) && button.text && (button.url || viberContent.buttonURL) && {
1424
- button: {
1425
- text: button.text,
1426
- url: button.url || viberContent.buttonURL || '',
1427
- },
1428
- }),
1429
- buttonURL: button.url || viberContent.buttonURL || '',
1400
+ accountName: accountName ? [accountName] : [],
1401
+ brandName: accountName ? [accountName] : [],
1402
+ };
1403
+ }
1404
+
1405
+ case WEBPUSH: {
1406
+ // WebPush content is stored in creatives format (brandIcon, onClickAction, ctas)
1407
+ // Must be transformed to campaign/test-message format matching getTemplateContent() in WebPush/Create/index.js
1408
+ const webpushContent = get(baseContent, 'content.webpush', {});
1409
+ const title = webpushContent?.title || '';
1410
+ const message = webpushContent?.message || '';
1411
+ const accountId = get(template, 'definition.accountId', null);
1412
+ const templateName = template?.name || '';
1413
+
1414
+ // iconImageUrl stored as brandIcon in creatives format
1415
+ const iconImageUrl = webpushContent?.brandIcon || webpushContent?.iconImageUrl || undefined;
1416
+
1417
+ // cta stored as onClickAction in creatives format (type: URL|SITE_URL, url)
1418
+ // or already as cta in campaign format (type: EXTERNAL_URL|SITE_URL, actionLink)
1419
+ let cta = null;
1420
+ const onClickAction = webpushContent?.onClickAction;
1421
+ const existingCta = webpushContent?.cta;
1422
+ if (onClickAction) {
1423
+ const ctaType = onClickAction.type === URL ? EXTERNAL_URL : (onClickAction.type || SITE_URL);
1424
+ cta = { type: ctaType, actionLink: onClickAction.url || '' };
1425
+ } else if (existingCta) {
1426
+ cta = { type: existingCta.type || EXTERNAL_URL, actionLink: existingCta.actionLink || '' };
1427
+ }
1428
+
1429
+ // expandableDetails: image → media[], ctas[] → mapped ctas
1430
+ const image = webpushContent?.image;
1431
+ const rawCtas = webpushContent?.ctas;
1432
+ const existingExpandable = webpushContent?.expandableDetails;
1433
+ let expandableDetails = null;
1434
+ const hasImage = !!image;
1435
+ const hasCtas = Array.isArray(rawCtas) && rawCtas.length > 0;
1436
+ if (hasImage || hasCtas) {
1437
+ expandableDetails = {
1438
+ media: hasImage ? [{ url: image, type: IMAGE }] : [],
1439
+ ctas: hasCtas ? rawCtas.map((ctaItem) => ({
1440
+ type: ctaItem?.type === URL ? EXTERNAL_URL : (ctaItem?.type || EXTERNAL_URL),
1441
+ action: ctaItem?.action || '',
1442
+ title: ctaItem?.actionText || ctaItem?.title || '',
1443
+ actionLink: ctaItem?.actionLink || '',
1444
+ })) : [],
1445
+ };
1446
+ } else if (existingExpandable) {
1447
+ expandableDetails = {
1448
+ media: existingExpandable?.media || [],
1449
+ ctas: existingExpandable?.ctas || [],
1450
+ };
1451
+ }
1452
+
1453
+ return {
1454
+ channel: WEBPUSH,
1455
+ accountId,
1456
+ content: {
1457
+ title,
1458
+ message,
1459
+ ...(iconImageUrl ? { iconImageUrl } : {}),
1460
+ ...(cta ? { cta } : {}),
1461
+ ...(expandableDetails ? { expandableDetails } : {}),
1462
+ },
1463
+ messageSubject: templateName || title,
1464
+ offers: [],
1430
1465
  };
1431
1466
  }
1432
1467
 
@@ -1442,7 +1477,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1442
1477
  * @returns {Boolean} - True if channel supports Test and Preview
1443
1478
  */
1444
1479
  isTestAndPreviewSupported = () => {
1445
- const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO];
1480
+ const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
1446
1481
  return supportedChannels.includes(this.state.channel.toUpperCase());
1447
1482
  }
1448
1483
 
@@ -2002,7 +2037,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2002
2037
  // Show preview icon only for channels that don't support Test and Preview
2003
2038
  (() => {
2004
2039
  // Channels that have Test and Preview integrated
2005
- const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO];
2040
+ const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
2006
2041
  const isTestAndPreviewSupported = testAndPreviewChannels.includes(currentChannel.toUpperCase());
2007
2042
 
2008
2043
  // Don't show preview icon if channel supports Test and Preview
@@ -2372,52 +2407,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2372
2407
  image = {},
2373
2408
  button = {},
2374
2409
  video = {},
2375
- type = '',
2376
- cards = [],
2377
2410
  } = {},
2378
2411
  } = {},
2379
2412
  } = template.versions.base;
2380
- const isViberCarousel = type === VIBER_MEDIA_TYPES.CAROUSEL;
2381
2413
  templateData.content = text;
2382
- if (isViberCarousel) {
2383
- const previewCards = Array.isArray(cards) ? cards.slice(0, 1) : [];
2384
- templateData.className = 'viber-carousel-static';
2385
- templateData.content = (
2386
- <CapRow className="viber-carousel-static-content">
2387
- <CapRow className="viber-carousel-static-message-box">
2388
- <CapLabel type="label1" className="viber-carousel-static-message">
2389
- {text}
2390
- </CapLabel>
2391
- </CapRow>
2392
- <CapRow className="viber-carousel-static-cards">
2393
- {previewCards.map((card, cardIdx) => (
2394
- <CapRow className="viber-carousel-static-card" key={`viber-static-card-${cardIdx}`}>
2395
- {card?.mediaUrl ? (
2396
- <CapImage src={card.mediaUrl} className="viber-carousel-static-image" />
2397
- ) : (
2398
- <CapRow className="viber-carousel-static-image-placeholder" />
2399
- )}
2400
- <CapLabel type="label1" className="viber-carousel-static-text">
2401
- {card?.text}
2402
- </CapLabel>
2403
- <CapRow className="viber-carousel-static-buttons">
2404
- {(Array.isArray(card?.buttons) && card.buttons.length ? card.buttons : [{}, {}]).slice(0, 2).map((carouselButton, btnIdx) => (
2405
- <CapLabel
2406
- key={`viber-static-btn-${cardIdx}-${btnIdx}`}
2407
- type="label1"
2408
- className={`viber-carousel-static-button ${btnIdx === 1 ? 'viber-carousel-static-button-secondary' : ''}`}
2409
- >
2410
- {(carouselButton?.title || '').trim()}
2411
- </CapLabel>
2412
- ))}
2413
- </CapRow>
2414
- </CapRow>
2415
- ))}
2416
- </CapRow>
2417
- </CapRow>
2418
- );
2419
- break;
2420
- }
2421
2414
  if (!isEmpty(image)) {
2422
2415
  const { url = '' } = image;
2423
2416
  templateData.url = url;
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Tests for Templates container - WEBPUSH channel additions
3
+ *
4
+ * Covers:
5
+ * - extractTemplateContentForPreview for WEBPUSH channel
6
+ * - isTestAndPreviewSupported with WEBPUSH channel
7
+ */
8
+
9
+ import React from 'react';
10
+ import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
11
+ import { Templates } from '../index';
12
+
13
+ jest.mock('../../CreativesContainer', () => ({
14
+ __esModule: true,
15
+ default: (props) => (
16
+ <div className="creatives-container-mock" {...props}>
17
+ CreativesContainer
18
+ </div>
19
+ ),
20
+ }));
21
+
22
+ // Helper: build a valid template for WEBPUSH
23
+ // extractTemplateContentForPreview reads:
24
+ // - template.versions.base.content.webpush → webpushContent
25
+ // - template.definition.accountId → accountId
26
+ // - template.name → templateName
27
+ const makeTemplate = (webpush = {}, extra = {}) => ({
28
+ name: 'My Template',
29
+ definition: { accountId: 'acc-123' },
30
+ versions: {
31
+ base: {
32
+ content: { webpush },
33
+ },
34
+ },
35
+ ...extra,
36
+ });
37
+
38
+ describe('Templates - WEBPUSH channel', () => {
39
+ const mockActions = {
40
+ getWeCrmAccounts: jest.fn(),
41
+ setChannelAccount: jest.fn(),
42
+ getAllTemplates: jest.fn(),
43
+ getUserList: jest.fn(),
44
+ getSenderDetails: jest.fn(),
45
+ resetTemplate: jest.fn(),
46
+ setArchivedMode: jest.fn(),
47
+ clearTemplateSelection: jest.fn(),
48
+ toggleTemplateSelection: jest.fn(),
49
+ };
50
+
51
+ const baseProps = {
52
+ route: { name: 'webpush' },
53
+ Templates: { templates: [] },
54
+ actions: mockActions,
55
+ location: { pathname: '/webpush', query: {}, search: '' },
56
+ EmailCreate: { duplicateTemplateInProgress: false },
57
+ isFullMode: false,
58
+ intl: { formatMessage: jest.fn((msg) => msg.defaultMessage || '') },
59
+ };
60
+
61
+ beforeEach(() => {
62
+ jest.clearAllMocks();
63
+ });
64
+
65
+ const renderComponent = (channel = 'webpush') => {
66
+ const props = { ...baseProps, route: { name: channel } };
67
+ return shallowWithIntl(<Templates {...props} />);
68
+ };
69
+
70
+ // ─────────────────────────────────────────────────────────────────────────
71
+ describe('isTestAndPreviewSupported', () => {
72
+ it('should return true for WEBPUSH channel', () => {
73
+ const component = renderComponent('webpush');
74
+ component.setState({ channel: 'webpush' });
75
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
76
+ });
77
+
78
+ it('should return true for WEBPUSH in uppercase state', () => {
79
+ const component = renderComponent('webpush');
80
+ component.setState({ channel: 'WEBPUSH' });
81
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
82
+ });
83
+
84
+ it('should return true for EMAIL channel', () => {
85
+ const component = renderComponent('webpush');
86
+ component.setState({ channel: 'email' });
87
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
88
+ });
89
+
90
+ it('should return true for SMS channel', () => {
91
+ const component = renderComponent('webpush');
92
+ component.setState({ channel: 'sms' });
93
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
94
+ });
95
+
96
+ it('should return true for INAPP channel', () => {
97
+ const component = renderComponent('webpush');
98
+ component.setState({ channel: 'inapp' });
99
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
100
+ });
101
+
102
+ it('should return true for VIBER channel', () => {
103
+ const component = renderComponent('webpush');
104
+ component.setState({ channel: 'viber' });
105
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
106
+ });
107
+
108
+ it('should return true for ZALO channel', () => {
109
+ const component = renderComponent('webpush');
110
+ component.setState({ channel: 'zalo' });
111
+ expect(component.instance().isTestAndPreviewSupported()).toBe(true);
112
+ });
113
+
114
+ it('should return false for WHATSAPP channel', () => {
115
+ const component = renderComponent('webpush');
116
+ component.setState({ channel: 'whatsapp' });
117
+ expect(component.instance().isTestAndPreviewSupported()).toBe(false);
118
+ });
119
+
120
+ it('should return false for RCS channel', () => {
121
+ const component = renderComponent('webpush');
122
+ component.setState({ channel: 'rcs' });
123
+ expect(component.instance().isTestAndPreviewSupported()).toBe(false);
124
+ });
125
+ });
126
+
127
+ // ─────────────────────────────────────────────────────────────────────────
128
+ describe('extractTemplateContentForPreview - WEBPUSH channel', () => {
129
+ it('should return null when template has no versions.base', () => {
130
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
131
+ const component = renderComponent('webpush');
132
+ const result = component.instance().extractTemplateContentForPreview(
133
+ { name: 'T', definition: {} },
134
+ 'WEBPUSH'
135
+ );
136
+ expect(result).toBeNull();
137
+ consoleSpy.mockRestore();
138
+ });
139
+
140
+ it('should extract title and message from versions.base.content.webpush', () => {
141
+ const component = renderComponent('webpush');
142
+ const template = makeTemplate({ title: 'Push Title', message: 'Push body' });
143
+
144
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
145
+ expect(result).toBeDefined();
146
+ expect(result.channel).toBe('WEBPUSH');
147
+ expect(result.accountId).toBe('acc-123');
148
+ expect(result.content.title).toBe('Push Title');
149
+ expect(result.content.message).toBe('Push body');
150
+ expect(result.messageSubject).toBe('My Template');
151
+ expect(result.offers).toEqual([]);
152
+ });
153
+
154
+ it('should fall back to title for messageSubject when template name is empty', () => {
155
+ const component = renderComponent('webpush');
156
+ const template = makeTemplate({ title: 'Title Only', message: 'M' }, { name: '' });
157
+
158
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
159
+ expect(result.messageSubject).toBe('Title Only');
160
+ });
161
+
162
+ it('should extract brandIcon as iconImageUrl', () => {
163
+ const component = renderComponent('webpush');
164
+ const template = makeTemplate({
165
+ title: 'T',
166
+ message: 'M',
167
+ brandIcon: 'https://example.com/brand.png',
168
+ });
169
+
170
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
171
+ expect(result.content.iconImageUrl).toBe('https://example.com/brand.png');
172
+ });
173
+
174
+ it('should use iconImageUrl field when brandIcon is absent', () => {
175
+ const component = renderComponent('webpush');
176
+ const template = makeTemplate({
177
+ title: 'T',
178
+ message: 'M',
179
+ iconImageUrl: 'https://example.com/icon.png',
180
+ });
181
+
182
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
183
+ expect(result.content.iconImageUrl).toBe('https://example.com/icon.png');
184
+ });
185
+
186
+ it('should NOT include iconImageUrl when neither brandIcon nor iconImageUrl present', () => {
187
+ const component = renderComponent('webpush');
188
+ const template = makeTemplate({ title: 'T', message: 'M' });
189
+
190
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
191
+ expect(result.content.iconImageUrl).toBeUndefined();
192
+ });
193
+
194
+ it('should convert onClickAction type URL to EXTERNAL_URL cta', () => {
195
+ const component = renderComponent('webpush');
196
+ const template = makeTemplate({
197
+ title: 'T',
198
+ message: 'M',
199
+ onClickAction: { type: 'URL', url: 'https://example.com' },
200
+ });
201
+
202
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
203
+ expect(result.content.cta).toEqual({ type: 'EXTERNAL_URL', actionLink: 'https://example.com' });
204
+ });
205
+
206
+ it('should preserve onClickAction type SITE_URL as-is', () => {
207
+ const component = renderComponent('webpush');
208
+ const template = makeTemplate({
209
+ title: 'T',
210
+ message: 'M',
211
+ onClickAction: { type: 'SITE_URL', url: 'https://site.com' },
212
+ });
213
+
214
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
215
+ expect(result.content.cta).toEqual({ type: 'SITE_URL', actionLink: 'https://site.com' });
216
+ });
217
+
218
+ it('should use existingCta when onClickAction is absent', () => {
219
+ const component = renderComponent('webpush');
220
+ const template = makeTemplate({
221
+ title: 'T',
222
+ message: 'M',
223
+ cta: { type: 'EXTERNAL_URL', actionLink: 'https://existing.com' },
224
+ });
225
+
226
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
227
+ expect(result.content.cta).toEqual({ type: 'EXTERNAL_URL', actionLink: 'https://existing.com' });
228
+ });
229
+
230
+ it('should default existingCta type to EXTERNAL_URL when type is missing', () => {
231
+ const component = renderComponent('webpush');
232
+ const template = makeTemplate({
233
+ title: 'T',
234
+ message: 'M',
235
+ cta: { actionLink: 'https://example.com' },
236
+ });
237
+
238
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
239
+ expect(result.content.cta.type).toBe('EXTERNAL_URL');
240
+ });
241
+
242
+ it('should NOT include cta when neither onClickAction nor cta present', () => {
243
+ const component = renderComponent('webpush');
244
+ const template = makeTemplate({ title: 'T', message: 'M' });
245
+
246
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
247
+ expect(result.content.cta).toBeUndefined();
248
+ });
249
+
250
+ it('should build expandableDetails from image and ctas', () => {
251
+ const component = renderComponent('webpush');
252
+ const template = makeTemplate({
253
+ title: 'T',
254
+ message: 'M',
255
+ image: 'https://example.com/image.jpg',
256
+ ctas: [{ type: 'URL', actionText: 'Click', actionLink: 'https://a.com', action: '' }],
257
+ });
258
+
259
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
260
+ expect(result.content.expandableDetails).toBeDefined();
261
+ expect(result.content.expandableDetails.media).toEqual([
262
+ { url: 'https://example.com/image.jpg', type: 'IMAGE' },
263
+ ]);
264
+ expect(result.content.expandableDetails.ctas[0].title).toBe('Click');
265
+ expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
266
+ });
267
+
268
+ it('should convert CTA type URL → EXTERNAL_URL in expandableDetails.ctas', () => {
269
+ const component = renderComponent('webpush');
270
+ const template = makeTemplate({
271
+ title: 'T',
272
+ message: 'M',
273
+ ctas: [{ type: 'URL', actionText: 'Go', actionLink: 'https://x.com' }],
274
+ });
275
+
276
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
277
+ expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
278
+ });
279
+
280
+ it('should keep EXTERNAL_URL type unchanged in expandableDetails.ctas', () => {
281
+ const component = renderComponent('webpush');
282
+ const template = makeTemplate({
283
+ title: 'T',
284
+ message: 'M',
285
+ ctas: [{ type: 'EXTERNAL_URL', title: 'Go', actionLink: 'https://x.com' }],
286
+ });
287
+
288
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
289
+ expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
290
+ });
291
+
292
+ it('should build image-only expandableDetails when no ctas', () => {
293
+ const component = renderComponent('webpush');
294
+ const template = makeTemplate({
295
+ title: 'T',
296
+ message: 'M',
297
+ image: 'https://example.com/img.jpg',
298
+ });
299
+
300
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
301
+ expect(result.content.expandableDetails.media).toHaveLength(1);
302
+ expect(result.content.expandableDetails.ctas).toEqual([]);
303
+ });
304
+
305
+ it('should build ctas-only expandableDetails when no image', () => {
306
+ const component = renderComponent('webpush');
307
+ const template = makeTemplate({
308
+ title: 'T',
309
+ message: 'M',
310
+ ctas: [{ type: 'EXTERNAL_URL', actionText: 'Btn', actionLink: 'https://b.com' }],
311
+ });
312
+
313
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
314
+ expect(result.content.expandableDetails.media).toEqual([]);
315
+ expect(result.content.expandableDetails.ctas).toHaveLength(1);
316
+ });
317
+
318
+ it('should use existingExpandable when no image and no ctas', () => {
319
+ const component = renderComponent('webpush');
320
+ const existingExpandable = {
321
+ media: [{ url: 'https://example.com/existing.jpg', type: 'IMAGE' }],
322
+ ctas: [{ title: 'Existing', actionLink: 'https://existing.com', type: 'EXTERNAL_URL' }],
323
+ };
324
+ const template = makeTemplate({
325
+ title: 'T',
326
+ message: 'M',
327
+ expandableDetails: existingExpandable,
328
+ });
329
+
330
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
331
+ expect(result.content.expandableDetails).toEqual(existingExpandable);
332
+ });
333
+
334
+ it('should NOT include expandableDetails when none present', () => {
335
+ const component = renderComponent('webpush');
336
+ const template = makeTemplate({ title: 'T', message: 'M' });
337
+
338
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
339
+ expect(result.content.expandableDetails).toBeUndefined();
340
+ });
341
+
342
+ it('should handle empty webpush content gracefully', () => {
343
+ const component = renderComponent('webpush');
344
+ const template = makeTemplate({}, { name: '' });
345
+
346
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
347
+ expect(result).toBeDefined();
348
+ expect(result.content.title).toBe('');
349
+ expect(result.content.message).toBe('');
350
+ });
351
+
352
+ it('should handle missing definition.accountId gracefully (returns null)', () => {
353
+ const component = renderComponent('webpush');
354
+ const template = {
355
+ name: 'T',
356
+ versions: { base: { content: { webpush: { title: 'T', message: 'M' } } } },
357
+ // no definition
358
+ };
359
+
360
+ const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
361
+ expect(result).toBeDefined();
362
+ expect(result.accountId).toBeNull();
363
+ });
364
+
365
+ it('should return null for unsupported channel', () => {
366
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
367
+ const component = renderComponent('webpush');
368
+ const template = makeTemplate({ title: 'T', message: 'M' });
369
+
370
+ const result = component.instance().extractTemplateContentForPreview(template, 'UNSUPPORTED_CHANNEL');
371
+ expect(result).toBeNull();
372
+ consoleSpy.mockRestore();
373
+ });
374
+ });
375
+ });
@@ -47,22 +47,7 @@ export const VIBER_MEDIA_TYPES = {
47
47
  TEXT: 'TEXT',
48
48
  IMAGE: 'IMAGE',
49
49
  VIDEO: 'VIDEO',
50
- CAROUSEL: 'CAROUSEL',
51
50
  };
52
- export const VIBER_CAROUSEL_MIN_CARDS = 2;
53
- export const VIBER_CAROUSEL_MAX_CARDS = 5;
54
- export const VIBER_CAROUSEL_MAX_BUTTONS = 2;
55
- export const VIBER_CAROUSEL_CARD_TITLE_MIN_LENGTH = 2;
56
- export const VIBER_CAROUSEL_CARD_TITLE_MAX_LENGTH = 38;
57
- export const VIBER_CAROUSEL_FIRST_BUTTON_TITLE_MAX_LENGTH = 10;
58
- export const VIBER_CAROUSEL_SECOND_BUTTON_TITLE_MAX_LENGTH = 12;
59
- export const VIBER_CAROUSEL_BUTTON_URL_MAX_LENGTH = 1000;
60
- /** Recommended / validated carousel image height in pixels (passed to image upload). */
61
- export const VIBER_CAROUSEL_IMG_HEIGHT = 600;
62
- /** Recommended / validated carousel image width in pixels (passed to image upload). */
63
- export const VIBER_CAROUSEL_IMG_WIDTH = 696;
64
- /** Maximum carousel image file size (bytes). 10_000_000 ≈ 10 MB (decimal). */
65
- export const VIBER_CAROUSEL_IMG_SIZE = 10000000;
66
51
  export const ALLOWED_IMAGE_EXTENSIONS_REGEX_VIBER = /\.(jpe?g|png)$/i;
67
52
  export const ALLOWED_EXTENSIONS_VIDEO_REGEX_VIBER = /\.(mp4|3gp|m4v|mov)$/i;
68
53
  export const NONE = 'NONE';
@@ -84,10 +69,6 @@ export const mediaRadioOptions = [
84
69
  value: VIBER_MEDIA_TYPES.VIDEO,
85
70
  label: <FormattedMessage {...messages.mediaVideo} />,
86
71
  },
87
- {
88
- value: VIBER_MEDIA_TYPES.CAROUSEL,
89
- label: <FormattedMessage {...messages.mediaCarousel} />,
90
- },
91
72
  ];
92
73
 
93
74
  export const buttonRadioOptions = [