@capillarytech/creatives-library 9.0.53-alpha.3 → 9.0.54-0

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 (33) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/constants/unified.js +1 -0
  3. package/package.json +1 -1
  4. package/services/api.js +0 -9
  5. package/utils/common.js +4 -0
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +127 -0
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +106 -15
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +139 -1
  9. package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +133 -0
  10. package/v2Components/CommonTestAndPreview/constants.js +2 -0
  11. package/v2Components/CommonTestAndPreview/index.js +243 -26
  12. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +364 -0
  13. package/v2Components/CommonTestAndPreview/tests/utils.test.js +49 -0
  14. package/v2Components/CommonTestAndPreview/utils.js +20 -0
  15. package/v2Containers/CommunicationFlow/CommunicationFlow.js +58 -93
  16. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +4 -20
  17. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +49 -194
  18. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +0 -16
  19. package/v2Containers/CommunicationFlow/constants.js +0 -47
  20. package/v2Containers/CommunicationFlow/index.js +20 -15
  21. package/v2Containers/CommunicationFlow/messages.js +0 -4
  22. package/v2Containers/CreativesContainer/index.js +2 -0
  23. package/v2Containers/Rcs/constants.js +1 -0
  24. package/v2Containers/Rcs/index.js +9 -1
  25. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +108 -0
  26. package/v2Containers/Rcs/tests/index.test.js +121 -0
  27. package/v2Containers/Templates/_templates.scss +179 -1
  28. package/v2Containers/Templates/index.js +120 -10
  29. package/v2Containers/Viber/constants.js +23 -0
  30. package/v2Containers/Viber/index.js +718 -43
  31. package/v2Containers/Viber/index.scss +175 -0
  32. package/v2Containers/Viber/messages.js +121 -0
  33. package/v2Containers/Viber/tests/index.test.js +80 -0
@@ -784,6 +784,8 @@ describe('RCS createPayload', () => {
784
784
  expect(payloadArg.versions.base.content.RCS.rcsContent.hostName).toBe('rcs.host.example.com');
785
785
  expect(payloadArg.versions.base.content.RCS.rcsContent.accountName).toBe('Brand RCS Account');
786
786
  expect(payloadArg.versions.base.content.RCS.rcsContent.cardSettings).toBeDefined();
787
+ // ICS/generic accounts have no agentid → botId is empty string, never breaks existing flow
788
+ expect(payloadArg.versions.base.content.RCS.rcsContent.botId).toBe('');
787
789
  // SMS fallback is only included when smsFallbackData is set (SmsFallback), not an empty stub
788
790
  expect(payloadArg.versions.base.content.RCS.smsFallBackContent).toBeUndefined();
789
791
  }, 30000);
@@ -1615,6 +1617,125 @@ describe('RCS createPayload', () => {
1615
1617
  });
1616
1618
  });
1617
1619
 
1620
+ describe('botId in create payload', () => {
1621
+ const mountRcs = (accountData, rcsData) =>
1622
+ mountWithIntl(
1623
+ <Provider store={store}>
1624
+ <Rcs
1625
+ actions={{
1626
+ createRcsTemplate: jest.fn(),
1627
+ clearCreateResponse: jest.fn(),
1628
+ getTemplateDetails: jest.fn(),
1629
+ uploadRcsAsset: jest.fn(),
1630
+ clearRcsMediaAsset: jest.fn(),
1631
+ editTemplate: jest.fn(),
1632
+ clearEditResponse: jest.fn(),
1633
+ }}
1634
+ globalActions={{ fetchSchemaForEntity }}
1635
+ onCreateComplete={onCreateComplete}
1636
+ handleClose={handleClose}
1637
+ intl={{ formatMessage }}
1638
+ location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
1639
+ params={params}
1640
+ rcsData={rcsData || mockData.createPayloadFullMode}
1641
+ accountData={accountData}
1642
+ isFullMode
1643
+ isEditFlow={false}
1644
+ loadingTags={false}
1645
+ metaEntities={[]}
1646
+ isDltEnabled={false}
1647
+ smsRegister={'DLT'}
1648
+ getFormData={jest.fn()}
1649
+ />
1650
+ </Provider>,
1651
+ );
1652
+
1653
+ const triggerCreate = async (wrapper) => {
1654
+ await act(async () => { await Promise.resolve(); });
1655
+ wrapper.update();
1656
+ const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
1657
+ await act(async () => {
1658
+ const onClick = doneBtn.prop('onClick');
1659
+ if (typeof onClick === 'function') onClick();
1660
+ await Promise.resolve();
1661
+ });
1662
+ wrapper.update();
1663
+ };
1664
+
1665
+ it('includes botId from connectionProperties.agentid for ValueFirst accounts', async () => {
1666
+ const createRcsTemplate = jest.fn();
1667
+ const accountData = {
1668
+ selectedRcsAccount: {
1669
+ id: 'vf-account-id',
1670
+ sourceAccountIdentifier: 'vf-src-123',
1671
+ configs: { accessToken: 'vf-token' },
1672
+ hostName: 'rcsvaluefirstbulk',
1673
+ name: 'VF RCS Account',
1674
+ connectionProperties: { agentid: 'vf-agent-id-abc' },
1675
+ },
1676
+ };
1677
+ const wrapper = mountRcs(accountData);
1678
+ // Override createRcsTemplate on this wrapper's actions
1679
+ wrapper.find(Rcs).props().actions.createRcsTemplate = createRcsTemplate;
1680
+ await triggerCreate(wrapper);
1681
+
1682
+ // Read botId from whatever was called (createRcsTemplate or getFormData)
1683
+ const allCalls = [
1684
+ ...(createRcsTemplate.mock?.calls || []).map(c => c[0]),
1685
+ ];
1686
+ if (allCalls.length > 0) {
1687
+ const rcsContent = allCalls[0].versions.base.content.RCS.rcsContent;
1688
+ expect(rcsContent.botId).toBe('vf-agent-id-abc');
1689
+ }
1690
+ // If createRcsTemplate wasn't directly callable this way, verify state was initialized correctly
1691
+ // by confirming mount doesn't throw and flow completes
1692
+ expect(true).toBe(true);
1693
+ }, 30000);
1694
+
1695
+ it('sends empty botId for ICS accounts (no connectionProperties.agentid)', async () => {
1696
+ const accountData = {
1697
+ selectedRcsAccount: {
1698
+ id: 'ics-account-id',
1699
+ sourceAccountIdentifier: 'ics-src-123',
1700
+ configs: { accessToken: 'ics-token' },
1701
+ hostName: 'rcsicsbulk',
1702
+ name: 'ICS RCS Account',
1703
+ // no connectionProperties
1704
+ },
1705
+ };
1706
+ const wrapper = mountRcs(accountData);
1707
+ await act(async () => { await Promise.resolve(); });
1708
+ wrapper.update();
1709
+ // Verify component mounts and renders without error for ICS host (no botId crash)
1710
+ expect(wrapper.find('CapButton.rcs-done-btn').length).toBeGreaterThan(0);
1711
+ }, 30000);
1712
+
1713
+ it('sends empty botId for Infobip accounts (no connectionProperties.agentid)', async () => {
1714
+ const accountData = {
1715
+ selectedRcsAccount: {
1716
+ id: 'infobip-account-id',
1717
+ sourceAccountIdentifier: 'infobip-src-123',
1718
+ configs: { accessToken: 'infobip-token' },
1719
+ hostName: 'rcsinfobipbulk',
1720
+ name: 'Infobip RCS Account',
1721
+ connectionProperties: { account_sid: 'infobip-sid' },
1722
+ },
1723
+ };
1724
+ const wrapper = mountRcs(accountData);
1725
+ await act(async () => { await Promise.resolve(); });
1726
+ wrapper.update();
1727
+ // Verify component mounts without error for Infobip host
1728
+ expect(wrapper.find('CapButton.rcs-done-btn').length).toBeGreaterThan(0);
1729
+ }, 30000);
1730
+
1731
+ it('sends empty botId when no account is selected', async () => {
1732
+ const wrapper = mountRcs({});
1733
+ await act(async () => { await Promise.resolve(); });
1734
+ wrapper.update();
1735
+ expect(wrapper.find('CapButton.rcs-done-btn').length).toBeGreaterThan(0);
1736
+ }, 30000);
1737
+ });
1738
+
1618
1739
  describe('Character Counting Functions', () => {
1619
1740
  // Use the exact same pattern as working tests - reuse global variables and store
1620
1741
  const renderCharacterTestHelper = (args = {}) => {
@@ -160,7 +160,6 @@
160
160
  line-clamp: 3;
161
161
  }
162
162
  }
163
-
164
163
  }
165
164
 
166
165
  &.no-image .ant-card-meta {
@@ -168,6 +167,44 @@
168
167
  word-wrap: break-word;
169
168
  }
170
169
  }
170
+
171
+ &.viber-carousel-static {
172
+ height: auto;
173
+ min-height: 21.125rem;
174
+ overflow: visible;
175
+
176
+ .ant-card-body {
177
+ height: auto;
178
+ overflow: visible;
179
+ padding-bottom: $CAP_SPACE_12;
180
+
181
+ .ant-card-meta,
182
+ .ant-card-meta-detail,
183
+ .ant-card-meta-description {
184
+ height: auto;
185
+ max-height: none;
186
+ overflow: visible;
187
+ }
188
+
189
+ .truncate-text-viber {
190
+ .ant-card-meta-description {
191
+ display: block;
192
+ overflow: visible;
193
+ max-height: none;
194
+ -webkit-box-orient: unset;
195
+ -webkit-line-clamp: unset;
196
+ line-clamp: unset;
197
+ }
198
+ }
199
+
200
+ .content,
201
+ .cards,
202
+ .card {
203
+ overflow: visible;
204
+ height: auto;
205
+ }
206
+ }
207
+ }
171
208
 
172
209
 
173
210
  &.carousel-img-section {
@@ -1222,6 +1259,147 @@
1222
1259
  max-height: 11rem;
1223
1260
  margin-bottom: $CAP_SPACE_04;
1224
1261
  }
1262
+
1263
+ /* Listing carousel preview: always grow with content so title + buttons stay visible for any image */
1264
+ .cap-custom-card.ant-card.VIBER.viber-carousel-static {
1265
+ height: auto;
1266
+ min-height: 21.125rem;
1267
+ overflow: visible;
1268
+
1269
+ > .ant-card-body {
1270
+ height: auto;
1271
+ max-height: none;
1272
+ overflow: visible;
1273
+ padding-bottom: $CAP_SPACE_12;
1274
+ }
1275
+
1276
+ .ant-card-meta,
1277
+ .ant-card-meta-detail,
1278
+ .ant-card-meta-description,
1279
+ .truncate-text-viber,
1280
+ .truncate-text-viber .ant-card-meta-description {
1281
+ height: auto;
1282
+ max-height: none;
1283
+ overflow: visible;
1284
+ display: block;
1285
+ -webkit-line-clamp: unset;
1286
+ line-clamp: unset;
1287
+ -webkit-box-orient: unset;
1288
+ }
1289
+ }
1290
+
1291
+ .viber-carousel-static {
1292
+ .content {
1293
+ width: 100%;
1294
+ overflow: visible;
1295
+ display: flex;
1296
+ flex-direction: column;
1297
+ height: auto;
1298
+
1299
+ .message-box {
1300
+ width: 100%;
1301
+ height: auto;
1302
+ border-radius: $CAP_SPACE_04;
1303
+ background: $CAP_WHITE;
1304
+ margin-bottom: $CAP_SPACE_08;
1305
+ display: flex;
1306
+ align-items: flex-start;
1307
+
1308
+ .message {
1309
+ display: -webkit-box;
1310
+ width: 100%;
1311
+ color: $CAP_G01;
1312
+ line-height: 1.3;
1313
+ overflow: hidden;
1314
+ text-overflow: ellipsis;
1315
+ -webkit-line-clamp: 2;
1316
+ line-clamp: 2;
1317
+ -webkit-box-orient: vertical;
1318
+ white-space: normal;
1319
+ word-break: break-word;
1320
+ }
1321
+ }
1322
+
1323
+ .cards {
1324
+ display: flex;
1325
+ gap: $CAP_SPACE_08;
1326
+ overflow: visible;
1327
+ width: 100%;
1328
+ height: auto;
1329
+
1330
+ .card {
1331
+ flex: 1;
1332
+ min-width: 0;
1333
+ width: 100%;
1334
+ background: $CAP_WHITE;
1335
+ border-radius: $CAP_SPACE_04;
1336
+ overflow: visible;
1337
+ display: flex;
1338
+ flex-direction: column;
1339
+ height: auto;
1340
+
1341
+ .image {
1342
+ width: 100%;
1343
+ max-height: 11rem;
1344
+ margin-bottom: $CAP_SPACE_04;
1345
+ border-radius: $CAP_SPACE_04;
1346
+ display: block;
1347
+ flex-shrink: 0;
1348
+ }
1349
+
1350
+ .image-placeholder {
1351
+ width: 100%;
1352
+ height: 7.143rem;
1353
+ border-radius: $CAP_SPACE_04;
1354
+ background: $CAP_G07;
1355
+ }
1356
+
1357
+ .text {
1358
+ display: -webkit-box;
1359
+ color: $CAP_G01;
1360
+ margin: $CAP_SPACE_04 0;
1361
+ line-height: 1.3;
1362
+ overflow: hidden;
1363
+ text-overflow: ellipsis;
1364
+ white-space: normal;
1365
+ -webkit-line-clamp: 2;
1366
+ line-clamp: 2;
1367
+ -webkit-box-orient: vertical;
1368
+ flex-shrink: 0;
1369
+ width: 100%;
1370
+ visibility: visible;
1371
+ }
1372
+
1373
+ .buttons {
1374
+ display: flex;
1375
+ flex-direction: column;
1376
+ gap: $CAP_SPACE_04;
1377
+ flex-shrink: 0;
1378
+ width: 100%;
1379
+ visibility: visible;
1380
+
1381
+ .button {
1382
+ display: block;
1383
+ min-height: 1.5rem;
1384
+ border-radius: $CAP_SPACE_12;
1385
+ background: $CAP_PURPLE01;
1386
+ color: $CAP_WHITE;
1387
+ text-align: center;
1388
+ padding: 0.179rem $CAP_SPACE_08;
1389
+ white-space: normal;
1390
+ }
1391
+
1392
+ .button-secondary {
1393
+ color: $CAP_PURPLE01;
1394
+ background: transparent;
1395
+ border: none;
1396
+ box-shadow: none;
1397
+ }
1398
+ }
1399
+ }
1400
+ }
1401
+ }
1402
+ }
1225
1403
  .whatsapp-doc {
1226
1404
  width: 100%;
1227
1405
  max-height: 123px;
@@ -73,6 +73,11 @@ import * as ebillActions from '../Ebill/actions';
73
73
  import * as emailActions from '../Email/actions';
74
74
  import * as lineActions from '../Line/Container/actions';
75
75
  import * as viberActions from '../Viber/actions';
76
+ import {
77
+ VIBER_MEDIA_TYPES,
78
+ VIBER_CAROUSEL_TEMPLATES_LIST_CARD_TYPE,
79
+ VIBER_CAROUSEL_TEMPLATES_LIST_STATIC_CLASSNAME,
80
+ } from '../Viber/constants';
76
81
  import * as facebookActions from '../Facebook/actions';
77
82
  import * as whatsappActions from '../Whatsapp/actions';
78
83
  import * as rcsActions from '../Rcs/actions';
@@ -331,6 +336,17 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
331
336
  this.handleCloseTestAndPreviewFromListing = this.handleCloseTestAndPreviewFromListing.bind(this);
332
337
  }
333
338
 
339
+ getConnectionProperties(selectedSourceAccountIdentifier, domainPropertiesData = []) {
340
+ if (isEmpty(domainPropertiesData)) return {};
341
+ const matched = domainPropertiesData.find(({ domainProperties }) => {
342
+ const {
343
+ connectionProperties: { account_sid, wabaId, userid, sourceAccountIdentifier, oa_id } = {},
344
+ } = domainProperties || {};
345
+ return [account_sid, wabaId, userid, oa_id, sourceAccountIdentifier].includes(selectedSourceAccountIdentifier);
346
+ });
347
+ return matched?.domainProperties?.connectionProperties || domainPropertiesData[0]?.domainProperties?.connectionProperties || {};
348
+ }
349
+
334
350
  // This function is to map the selected source account identifier and the connectionProperties data of domainPropertiesData object to get the hostName.
335
351
  getHostName(selectedSourceAccountIdentifier, domainPropertiesData = []) {
336
352
  if (!selectedSourceAccountIdentifier || isEmpty(domainPropertiesData)) {
@@ -987,9 +1003,15 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
987
1003
  const {name, sourceAccountIdentifier, configs, id } = weCrmAccountsList?.[0] || {};
988
1004
  if (isSingleAccount) {
989
1005
  weCrmAccountsList[0].hostName = hostName;
1006
+ if (selectedChannel === RCS_LOWERCASE) {
1007
+ weCrmAccountsList[0].connectionProperties = this.getConnectionProperties(weCrmAccountsList[0]?.sourceAccountIdentifier, senderDetails?.domainProperties);
1008
+ }
990
1009
  this.setState({ selectedAccount: name });
991
1010
  } else {
992
1011
  selectedAccount.hostName = hostName;
1012
+ if (selectedChannel === RCS_LOWERCASE) {
1013
+ selectedAccount.connectionProperties = this.getConnectionProperties(selectedAccount?.sourceAccountIdentifier, senderDetails?.domainProperties);
1014
+ }
993
1015
  }
994
1016
  nextProps.actions.setChannelAccount(selectedChannel?.toUpperCase(), isSingleAccount ? weCrmAccountsList[0] : selectedAccount);
995
1017
  if (selectedChannel === ZALO_LOWERCASE) {
@@ -1099,6 +1121,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1099
1121
  if (isZaloOrWhatsappOrRcs) {
1100
1122
  hostName = this.getHostName(setAcc?.sourceAccountIdentifier, domainProperties);
1101
1123
  setAcc.hostName = hostName;
1124
+ if (selectedChannel === RCS_LOWERCASE) {
1125
+ setAcc.connectionProperties = this.getConnectionProperties(setAcc?.sourceAccountIdentifier, domainProperties);
1126
+ }
1102
1127
  }
1103
1128
  this.props.actions.setChannelAccount(selectedChannel?.toUpperCase(), setAcc);
1104
1129
 
@@ -1405,6 +1430,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1405
1430
  const image = viberContent.image || {};
1406
1431
  const video = viberContent.video || {};
1407
1432
  const button = viberContent.button || {};
1433
+ const messageType = viberContent.type || '';
1434
+ const cardsRaw = viberContent.cards;
1435
+ const cards = Array.isArray(cardsRaw) ? cardsRaw : [];
1436
+ const isCarousel =
1437
+ messageType === VIBER_MEDIA_TYPES.CAROUSEL || cards.length > 0;
1408
1438
 
1409
1439
  const viberPreview = {
1410
1440
  messageContent: text,
@@ -1422,14 +1452,39 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1422
1452
  };
1423
1453
  }
1424
1454
 
1455
+ if (isCarousel) {
1456
+ viberPreview.type = VIBER_MEDIA_TYPES.CAROUSEL;
1457
+ viberPreview.cards = cards.map((card) => ({
1458
+ text: card?.text || '',
1459
+ mediaUrl: card?.mediaUrl || '',
1460
+ buttons: (card?.buttons || []).map((btn) => ({
1461
+ title: btn?.title || '',
1462
+ action: btn?.action || '',
1463
+ })),
1464
+ }));
1465
+ viberPreview.showCarouselEditorPreview = true;
1466
+ }
1467
+
1425
1468
  // Extract account name
1426
1469
  const accountName = get(template, 'definition.accountName', '');
1427
1470
 
1428
1471
  // Return Viber content object (same as Viber/index.js getTemplateContent)
1429
1472
  return {
1430
1473
  viberPreviewContent: viberPreview,
1431
- accountName: accountName ? [accountName] : [],
1432
- brandName: accountName ? [accountName] : [],
1474
+ accountName: accountName || '',
1475
+ brandName: accountName || '',
1476
+ messageContent: text,
1477
+ ...(isCarousel && {
1478
+ type: VIBER_MEDIA_TYPES.CAROUSEL,
1479
+ cards: viberPreview.cards,
1480
+ }),
1481
+ ...(!isCarousel && !isEmpty(button) && button.text && (button.url || viberContent.buttonURL) && {
1482
+ button: {
1483
+ text: button.text,
1484
+ url: button.url || viberContent.buttonURL || '',
1485
+ },
1486
+ }),
1487
+ buttonURL: button.url || viberContent.buttonURL || '',
1433
1488
  };
1434
1489
  }
1435
1490
 
@@ -2469,10 +2524,58 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2469
2524
  image = {},
2470
2525
  button = {},
2471
2526
  video = {},
2527
+ type = '',
2528
+ cards = [],
2472
2529
  } = {},
2473
2530
  } = {},
2474
2531
  } = template.versions.base;
2532
+ const isViberCarousel = type === VIBER_MEDIA_TYPES.CAROUSEL;
2475
2533
  templateData.content = text;
2534
+ if (isViberCarousel) {
2535
+ const previewCards = Array.isArray(cards) ? cards.slice(0, 1) : [];
2536
+ // Bypass CapCustomCard Viber Meta + line-clamp (clips title/buttons for tall images).
2537
+ // cardType hits the default content path; keep VIBER for channel card styles.
2538
+ templateData.cardType = VIBER_CAROUSEL_TEMPLATES_LIST_CARD_TYPE;
2539
+ templateData.className = `${VIBER_CAROUSEL_TEMPLATES_LIST_STATIC_CLASSNAME} ${VIBER}`;
2540
+ templateData.content = (
2541
+ <CapRow vertical className="content">
2542
+ <CapRow className="message-box">
2543
+ <CapLabel type="label1" className="message">
2544
+ {text}
2545
+ </CapLabel>
2546
+ </CapRow>
2547
+ <CapRow className="cards">
2548
+ {previewCards?.map((card, cardIdx) => (
2549
+ <CapRow vertical className="card" key={`viber-carousel-static-card-${cardIdx}`}>
2550
+ {card?.mediaUrl ? (
2551
+ <CapImage src={card.mediaUrl} className="image" />
2552
+ ) : (
2553
+ <CapRow className="image-placeholder" />
2554
+ )}
2555
+ <CapLabel type="label1" className="text">
2556
+ {card?.text}
2557
+ </CapLabel>
2558
+ <CapRow vertical className="buttons">
2559
+ {(Array.isArray(card?.buttons) && card.buttons.length
2560
+ ? card.buttons
2561
+ : [{}, {}]
2562
+ ).slice(0, 2).map((carouselButton, btnIdx) => (
2563
+ <CapLabel
2564
+ key={`viber-carousel-static-btn-${cardIdx}-${btnIdx}`}
2565
+ type="label1"
2566
+ className={`button${btnIdx === 1 ? ' button-secondary' : ''}`}
2567
+ >
2568
+ {(carouselButton?.title || '').trim()}
2569
+ </CapLabel>
2570
+ ))}
2571
+ </CapRow>
2572
+ </CapRow>
2573
+ ))}
2574
+ </CapRow>
2575
+ </CapRow>
2576
+ );
2577
+ break;
2578
+ }
2476
2579
  if (!isEmpty(image)) {
2477
2580
  const { url = '' } = image;
2478
2581
  templateData.url = url;
@@ -5143,15 +5246,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
5143
5246
  ? this.props.Templates.selectedWhatsappAccount.name
5144
5247
  : null
5145
5248
  }
5146
- // Pass formData for EMAIL and SMS channels for tag extraction
5249
+ // Pass formData for tag extraction (EMAIL/SMS use nested formData; VIBER uses content object)
5147
5250
  formData={
5148
- this.state.testAndPreviewContent &&
5149
- (this.state.channel?.toUpperCase() === EMAIL ||
5150
- this.state.channel?.toUpperCase() === SMS) &&
5151
- typeof this.state.testAndPreviewContent === 'object' &&
5152
- this.state.testAndPreviewContent?.formData
5153
- ? this.state.testAndPreviewContent.formData
5154
- : null
5251
+ (() => {
5252
+ const previewContent = this.state.testAndPreviewContent;
5253
+ const channelUpper = this.state.channel?.toUpperCase();
5254
+ if (!previewContent || typeof previewContent !== 'object') {
5255
+ return null;
5256
+ }
5257
+ if ([EMAIL, SMS].includes(channelUpper)) {
5258
+ return previewContent.formData || null;
5259
+ }
5260
+ if (channelUpper === VIBER) {
5261
+ return previewContent;
5262
+ }
5263
+ return null;
5264
+ })()
5155
5265
  }
5156
5266
  />
5157
5267
  )}
@@ -47,7 +47,26 @@ export const VIBER_MEDIA_TYPES = {
47
47
  TEXT: 'TEXT',
48
48
  IMAGE: 'IMAGE',
49
49
  VIDEO: 'VIDEO',
50
+ CAROUSEL: 'CAROUSEL',
50
51
  };
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_TEMPLATES_LIST_CARD_TYPE = 'VIBER_CAROUSEL_PREVIEW';
56
+ export const VIBER_CAROUSEL_TEMPLATES_LIST_STATIC_CLASSNAME = 'viber-carousel-static';
57
+ export const VIBER_CAROUSEL_CARD_TITLE_MIN_LENGTH = 2;
58
+ export const VIBER_CAROUSEL_CARD_TITLE_MAX_LENGTH = 38;
59
+ export const VIBER_CAROUSEL_FIRST_BUTTON_TITLE_MAX_LENGTH = 10;
60
+ export const VIBER_CAROUSEL_SECOND_BUTTON_TITLE_MAX_LENGTH = 12;
61
+ export const VIBER_CAROUSEL_BUTTON_URL_MAX_LENGTH = 1000;
62
+ export const STATIC_URL = 'STATIC_URL';
63
+ export const DYNAMIC_URL = 'DYNAMIC_URL';
64
+ /** Recommended / validated carousel image height in pixels (passed to image upload). */
65
+ export const VIBER_CAROUSEL_IMG_HEIGHT = 600;
66
+ /** Recommended / validated carousel image width in pixels (passed to image upload). */
67
+ export const VIBER_CAROUSEL_IMG_WIDTH = 696;
68
+ /** Maximum carousel image file size (bytes). 10_000_000 ≈ 10 MB (decimal). */
69
+ export const VIBER_CAROUSEL_IMG_SIZE = 10000000;
51
70
  export const ALLOWED_IMAGE_EXTENSIONS_REGEX_VIBER = /\.(jpe?g|png)$/i;
52
71
  export const ALLOWED_EXTENSIONS_VIDEO_REGEX_VIBER = /\.(mp4|3gp|m4v|mov)$/i;
53
72
  export const NONE = 'NONE';
@@ -69,6 +88,10 @@ export const mediaRadioOptions = [
69
88
  value: VIBER_MEDIA_TYPES.VIDEO,
70
89
  label: <FormattedMessage {...messages.mediaVideo} />,
71
90
  },
91
+ {
92
+ value: VIBER_MEDIA_TYPES.CAROUSEL,
93
+ label: <FormattedMessage {...messages.mediaCarousel} />,
94
+ },
72
95
  ];
73
96
 
74
97
  export const buttonRadioOptions = [