@capillarytech/creatives-library 9.0.35 → 9.0.36-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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/translations/en.json +2 -2
  3. package/utils/permissions.js +59 -0
  4. package/utils/tests/permissions.test.js +116 -0
  5. package/v2Components/AccessForbidden/_accessForbidden.scss +14 -0
  6. package/v2Components/AccessForbidden/index.js +5 -3
  7. package/v2Components/AccessForbidden/messages.js +3 -2
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +132 -0
  9. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +108 -15
  10. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +139 -1
  11. package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +131 -0
  12. package/v2Components/CommonTestAndPreview/index.js +240 -26
  13. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +364 -0
  14. package/v2Components/NoPermissionWrapper/_noPermissionWrapper.scss +12 -0
  15. package/v2Components/NoPermissionWrapper/index.js +26 -0
  16. package/v2Containers/App/constants.js +5 -0
  17. package/v2Containers/Assets/Gallery/_gallery.scss +6 -0
  18. package/v2Containers/Assets/Gallery/index.js +34 -10
  19. package/v2Containers/Cap/index.js +5 -2
  20. package/v2Containers/Cap/messages.js +4 -0
  21. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
  22. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
  23. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
  24. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
  25. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
  26. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +118 -118
  27. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
  28. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -18
  29. package/v2Containers/Templates/ChannelTypeIllustration.js +13 -2
  30. package/v2Containers/Templates/_templates.scss +201 -0
  31. package/v2Containers/Templates/index.js +244 -110
  32. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +1 -1
  33. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +174 -121
  34. package/v2Containers/TemplatesV2/index.js +16 -5
  35. package/v2Containers/Viber/constants.js +21 -0
  36. package/v2Containers/Viber/index.js +719 -49
  37. package/v2Containers/Viber/index.scss +175 -0
  38. package/v2Containers/Viber/messages.js +121 -0
  39. package/v2Containers/Viber/tests/index.test.js +80 -0
  40. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +268 -268
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.35",
4
+ "version": "9.0.36-alpha.1",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -63,8 +63,8 @@
63
63
  "app.v2containers.SmsTrai.Edit.traiEditTitle": "Message",
64
64
  "app.v2containers.SmsTrai.Edit.unicodeLabel": "Allow unicode characters",
65
65
  "app.v2containers.SmsTrai.Edit.unsupportedTagsValidationError": "Unsupported tags: {unsupportedTags}. Please remove them from this message.",
66
- "creatives.components.AccessForbidden.forbiddenDesc": "This page access has not been provided to you.",
67
- "creatives.components.AccessForbidden.forbiddenHeader": "Access Forbidden",
66
+ "creatives.components.AccessForbidden.forbiddenDesc": "Please contact your organisation administrator to request access.",
67
+ "creatives.components.AccessForbidden.forbiddenHeader": "You don't have permission to view this page.",
68
68
  "creatives.components.BreadCrumbs.header": "This is the BreadCrumbs component !",
69
69
  "creatives.components.CapTagList.Cancel": "Cancel",
70
70
  "creatives.components.CapTagList.Ok": "Ok",
@@ -0,0 +1,59 @@
1
+ import {
2
+ CREATIVE_VIEW,
3
+ CREATIVE_CREATE,
4
+ CREATIVE_EDIT,
5
+ CREATIVE_DELETE,
6
+ CREATIVES_UI_VIEW,
7
+ } from '../v2Containers/App/constants';
8
+
9
+ export const PERMISSIONS = {
10
+ CREATIVE_VIEW,
11
+ CREATIVE_CREATE,
12
+ CREATIVE_EDIT,
13
+ CREATIVE_DELETE,
14
+ };
15
+
16
+ export const PERMISSION_IDS = {
17
+ [CREATIVE_VIEW]: 1508,
18
+ [CREATIVE_CREATE]: 1509,
19
+ [CREATIVE_EDIT]: 1510,
20
+ [CREATIVE_DELETE]: 1511,
21
+ };
22
+
23
+ /**
24
+ * Mode-aware access resolver for the tri-modal Creatives app.
25
+ * Standalone: the `cap` slice is passed as a prop by the connected container.
26
+ * Library / MFE: the host passes a `cap` (or legacy `Global`) prop.
27
+ * Reads the prop first, then falls back — mirrors TemplatesV2's existing
28
+ * `cap.user || Global.user` resolution.
29
+ */
30
+ export const getUserAccess = (props = {}) => {
31
+ const { cap = {}, Global = {} } = props;
32
+ const user = (cap && cap.user) || (Global && Global.user) || {};
33
+ const isLoaded = !!(
34
+ user &&
35
+ typeof user === 'object' &&
36
+ Object.keys(user).length > 0
37
+ );
38
+ return {
39
+ accessiblePermissions: (isLoaded && user.accessiblePermissions) || [],
40
+ isLoaded,
41
+ };
42
+ };
43
+
44
+ export const hasPermission = (userAccess, code) => {
45
+ if (!userAccess) return false;
46
+ const granted = userAccess.accessiblePermissions || [];
47
+ const id = PERMISSION_IDS[code];
48
+ return (
49
+ granted.includes(code) ||
50
+ granted.includes(id) ||
51
+ granted.includes(String(id))
52
+ );
53
+ };
54
+
55
+ // Zero-impact view gate: honour the new CREATIVE_VIEW OR the legacy CREATIVES_UI_VIEW.
56
+ export const canViewCreatives = userAccess =>
57
+ hasPermission(userAccess, CREATIVE_VIEW) ||
58
+ (userAccess &&
59
+ (userAccess.accessiblePermissions || []).includes(CREATIVES_UI_VIEW));
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Test suite for RBAC permission helpers (app/utils/permissions.js).
3
+ * Covers getUserAccess (loaded/unloaded, cap vs Global, missing perms),
4
+ * hasPermission (null guard, code/id/string-id match, empty), and
5
+ * canViewCreatives (new perm, legacy perm, null guard).
6
+ */
7
+ import {
8
+ PERMISSIONS,
9
+ PERMISSION_IDS,
10
+ getUserAccess,
11
+ hasPermission,
12
+ canViewCreatives,
13
+ } from '../permissions';
14
+ import {
15
+ CREATIVE_VIEW,
16
+ CREATIVE_CREATE,
17
+ CREATIVE_EDIT,
18
+ CREATIVE_DELETE,
19
+ CREATIVES_UI_VIEW,
20
+ } from '../../v2Containers/App/constants';
21
+
22
+ describe('permissions - exported maps', () => {
23
+ it('exposes the four creative permission codes', () => {
24
+ expect(PERMISSIONS).toEqual({
25
+ CREATIVE_VIEW,
26
+ CREATIVE_CREATE,
27
+ CREATIVE_EDIT,
28
+ CREATIVE_DELETE,
29
+ });
30
+ });
31
+
32
+ it('maps each permission code to its id', () => {
33
+ expect(PERMISSION_IDS[CREATIVE_VIEW]).toBe(1508);
34
+ expect(PERMISSION_IDS[CREATIVE_CREATE]).toBe(1509);
35
+ expect(PERMISSION_IDS[CREATIVE_EDIT]).toBe(1510);
36
+ expect(PERMISSION_IDS[CREATIVE_DELETE]).toBe(1511);
37
+ });
38
+ });
39
+
40
+ describe('getUserAccess', () => {
41
+ it('returns unloaded access with empty perms when no props are passed', () => {
42
+ expect(getUserAccess()).toEqual({ accessiblePermissions: [], isLoaded: false });
43
+ });
44
+
45
+ it('returns unloaded access for an empty props object', () => {
46
+ expect(getUserAccess({})).toEqual({ accessiblePermissions: [], isLoaded: false });
47
+ });
48
+
49
+ it('reads accessiblePermissions from cap.user when loaded', () => {
50
+ const result = getUserAccess({ cap: { user: { accessiblePermissions: [1508, 1509] } } });
51
+ expect(result).toEqual({ accessiblePermissions: [1508, 1509], isLoaded: true });
52
+ });
53
+
54
+ it('falls back to Global.user when cap.user is absent', () => {
55
+ const result = getUserAccess({ Global: { user: { accessiblePermissions: ['CREATIVE_VIEW'] } } });
56
+ expect(result).toEqual({ accessiblePermissions: ['CREATIVE_VIEW'], isLoaded: true });
57
+ });
58
+
59
+ it('is not loaded when the user object is empty', () => {
60
+ expect(getUserAccess({ cap: { user: {} } })).toEqual({ accessiblePermissions: [], isLoaded: false });
61
+ });
62
+
63
+ it('defaults accessiblePermissions to [] when a loaded user has none', () => {
64
+ const result = getUserAccess({ cap: { user: { id: 42 } } });
65
+ expect(result).toEqual({ accessiblePermissions: [], isLoaded: true });
66
+ });
67
+ });
68
+
69
+ describe('hasPermission', () => {
70
+ it('returns false when userAccess is null or undefined', () => {
71
+ expect(hasPermission(null, CREATIVE_VIEW)).toBe(false);
72
+ expect(hasPermission(undefined, CREATIVE_VIEW)).toBe(false);
73
+ });
74
+
75
+ it('returns false when accessiblePermissions is missing', () => {
76
+ expect(hasPermission({}, CREATIVE_VIEW)).toBe(false);
77
+ });
78
+
79
+ it('matches by permission code', () => {
80
+ expect(hasPermission({ accessiblePermissions: [CREATIVE_CREATE] }, CREATIVE_CREATE)).toBe(true);
81
+ });
82
+
83
+ it('matches by numeric permission id', () => {
84
+ expect(hasPermission({ accessiblePermissions: [1509] }, CREATIVE_CREATE)).toBe(true);
85
+ });
86
+
87
+ it('matches by string permission id', () => {
88
+ expect(hasPermission({ accessiblePermissions: ['1509'] }, CREATIVE_CREATE)).toBe(true);
89
+ });
90
+
91
+ it('returns false when the permission is not granted', () => {
92
+ expect(hasPermission({ accessiblePermissions: [CREATIVE_VIEW] }, CREATIVE_DELETE)).toBe(false);
93
+ expect(hasPermission({ accessiblePermissions: [] }, CREATIVE_VIEW)).toBe(false);
94
+ });
95
+ });
96
+
97
+ describe('canViewCreatives', () => {
98
+ it('allows when the new CREATIVE_VIEW permission is present', () => {
99
+ expect(canViewCreatives({ accessiblePermissions: [CREATIVE_VIEW] })).toBe(true);
100
+ expect(canViewCreatives({ accessiblePermissions: [1508] })).toBe(true);
101
+ });
102
+
103
+ it('allows via the legacy CREATIVES_UI_VIEW permission', () => {
104
+ expect(canViewCreatives({ accessiblePermissions: [CREATIVES_UI_VIEW] })).toBe(true);
105
+ });
106
+
107
+ it('returns a falsy value when userAccess is null', () => {
108
+ // short-circuits on `userAccess &&`, so returns null (falsy) rather than false
109
+ expect(canViewCreatives(null)).toBeFalsy();
110
+ });
111
+
112
+ it('returns false when neither view permission is present', () => {
113
+ expect(canViewCreatives({ accessiblePermissions: [CREATIVE_CREATE] })).toBe(false);
114
+ expect(canViewCreatives({})).toBe(false);
115
+ });
116
+ });
@@ -0,0 +1,14 @@
1
+ // Access-denied screen: rbac-lock icon + heading + subtext (matches the shared
2
+ // "no access" screen used across Loyalty+ / Org Settings).
3
+ .no-access-icon {
4
+ font-size: 48px;
5
+ color: #8c8c8c;
6
+ }
7
+ .no-access-title {
8
+ margin-top: 16px;
9
+ }
10
+ .no-access-text {
11
+ margin-top: 8px;
12
+ color: #6b778c;
13
+ max-width: 420px;
14
+ }
@@ -1,14 +1,16 @@
1
1
  import React from 'react';
2
2
  import { FormattedMessage } from 'react-intl';
3
- import { CapRow, CapHeading } from '@capillarytech/cap-ui-library';
3
+ import { CapRow, CapHeading, CapIcon } from '@capillarytech/cap-ui-library';
4
4
  import messages from './messages';
5
+ import './_accessForbidden.scss';
5
6
 
6
7
  const AccessForbidden = () => (
7
8
  <CapRow type='flex' vertical justify='center' align='middle' className="margin-t-32 text-align-center">
8
- <CapHeading type="h2">
9
+ <CapIcon type="rbac-lock" className="no-access-icon" />
10
+ <CapHeading type="h3" className="no-access-title">
9
11
  <FormattedMessage {...messages.forbiddenHeader} />
10
12
  </CapHeading>
11
- <CapHeading type="h3" className="margin-t-24">
13
+ <CapHeading type="h6" className="no-access-text">
12
14
  <FormattedMessage {...messages.forbiddenDesc} />
13
15
  </CapHeading>
14
16
  </CapRow>
@@ -5,10 +5,11 @@ const prefix = 'creatives.components.AccessForbidden';
5
5
  export default defineMessages({
6
6
  forbiddenHeader: {
7
7
  id: `${prefix}.forbiddenHeader`,
8
- defaultMessage: 'Access Forbidden',
8
+ defaultMessage: "You don't have permission to view this page.",
9
9
  },
10
10
  forbiddenDesc: {
11
11
  id: `${prefix}.forbiddenDesc`,
12
- defaultMessage: 'This page access has not been provided to you.',
12
+ defaultMessage:
13
+ 'Please contact your organisation administrator to request access.',
13
14
  },
14
15
  });
@@ -0,0 +1,132 @@
1
+ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
4
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
5
+ import CapImage from '@capillarytech/cap-ui-library/CapImage';
6
+
7
+ const getTrimmedText = (value = '') => (value ?? '').trim();
8
+ const hasTrimmedText = (value = '') => Boolean(getTrimmedText(value));
9
+
10
+ const getCarouselButtonsWithTitles = (card) => (
11
+ (card?.buttons ?? []).filter((button) => hasTrimmedText(button?.title)).slice(0, 2)
12
+ );
13
+
14
+ const getCarouselButtonSlotCount = (cards = []) => {
15
+ const buttonCounts = cards.map((card) => getCarouselButtonsWithTitles(card).length);
16
+ return Math.min(2, Math.max(0, ...buttonCounts, 0));
17
+ };
18
+
19
+ const ViberCarouselPreviewCards = ({ cards = [] }) => {
20
+ const previewCards = Array.isArray(cards) && cards.length ? cards : [{}];
21
+ const carouselButtonSlotCount = useMemo(
22
+ () => getCarouselButtonSlotCount(previewCards),
23
+ [previewCards],
24
+ );
25
+ const carouselTitleRefs = useRef([]);
26
+ const [carouselTitleLineCount, setCarouselTitleLineCount] = useState(1);
27
+
28
+ carouselTitleRefs.current = previewCards.map(() => null);
29
+
30
+ useLayoutEffect(() => {
31
+ let maxLines = 1;
32
+ carouselTitleRefs.current.forEach((node) => {
33
+ if (!node) {
34
+ return;
35
+ }
36
+ const { lineHeight } = window.getComputedStyle(node);
37
+ const parsedLineHeight = parseFloat(lineHeight) || 18;
38
+ const lines = Math.min(2, Math.max(1, Math.round(node.scrollHeight / parsedLineHeight)));
39
+ if (lines > maxLines) {
40
+ maxLines = lines;
41
+ }
42
+ });
43
+ setCarouselTitleLineCount((prev) => (prev === maxLines ? prev : maxLines));
44
+ }, [previewCards]);
45
+
46
+ return (
47
+ <CapRow type="flex" noWrap className="viber-carousel-preview-scroll">
48
+ {previewCards.map((card, index) => {
49
+ const cardButtons = getCarouselButtonsWithTitles(card);
50
+ const titleLineClass = `viber-carousel-preview-text-wrap--${carouselTitleLineCount}-line`;
51
+
52
+ return (
53
+ <CapRow type="flex-column" className="viber-carousel-preview-card" key={`viber-carousel-preview-card-${index}`}>
54
+ {hasTrimmedText(card?.mediaUrl) ? (
55
+ <CapImage
56
+ src={card?.mediaUrl}
57
+ className="viber-carousel-preview-image"
58
+ alt="Viber carousel card"
59
+ />
60
+ ) : (
61
+ <CapRow className="viber-carousel-preview-image-placeholder" />
62
+ )}
63
+ <CapRow className="viber-carousel-preview-card-body">
64
+ <CapRow className={`viber-carousel-preview-text-wrap ${titleLineClass}`}>
65
+ {hasTrimmedText(card?.text) ? (
66
+ <CapRow
67
+ className="viber-carousel-preview-text-inner"
68
+ ref={(node) => {
69
+ carouselTitleRefs.current[index] = node;
70
+ }}
71
+ >
72
+ <CapLabel type="label15" className="viber-carousel-preview-text">
73
+ {card?.text}
74
+ </CapLabel>
75
+ </CapRow>
76
+ ) : (
77
+ <CapLabel type="label15" className="viber-carousel-preview-text-placeholder" />
78
+ )}
79
+ </CapRow>
80
+ {carouselButtonSlotCount > 0 && (
81
+ <CapRow className="viber-carousel-preview-buttons">
82
+ {Array.from({ length: carouselButtonSlotCount }).map((_, btnIndex) => {
83
+ const cardButton = cardButtons[btnIndex];
84
+ const trimmedCardButtonTitle = cardButton
85
+ ? getTrimmedText(cardButton?.title)
86
+ : '';
87
+
88
+ if (!trimmedCardButtonTitle) {
89
+ return (
90
+ <CapRow
91
+ className="viber-carousel-preview-button viber-carousel-preview-button-placeholder"
92
+ key={`viber-carousel-preview-btn-placeholder-${index}-${btnIndex}`}
93
+ aria-hidden="true"
94
+ />
95
+ );
96
+ }
97
+
98
+ return (
99
+ <CapLabel
100
+ className={`viber-carousel-preview-button ${btnIndex === 1 ? 'viber-carousel-preview-button-secondary' : ''}`}
101
+ key={`viber-carousel-preview-btn-${index}-${btnIndex}-${trimmedCardButtonTitle}`}
102
+ >
103
+ {trimmedCardButtonTitle}
104
+ </CapLabel>
105
+ );
106
+ })}
107
+ </CapRow>
108
+ )}
109
+ </CapRow>
110
+ </CapRow>
111
+ );
112
+ })}
113
+ </CapRow>
114
+ );
115
+ };
116
+
117
+ ViberCarouselPreviewCards.propTypes = {
118
+ cards: PropTypes.arrayOf(PropTypes.shape({
119
+ text: PropTypes.string,
120
+ mediaUrl: PropTypes.string,
121
+ buttons: PropTypes.arrayOf(PropTypes.shape({
122
+ title: PropTypes.string,
123
+ action: PropTypes.string,
124
+ })),
125
+ })),
126
+ };
127
+
128
+ ViberCarouselPreviewCards.defaultProps = {
129
+ cards: [],
130
+ };
131
+
132
+ export default ViberCarouselPreviewCards;
@@ -14,14 +14,25 @@ import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
14
14
  import CapImage from '@capillarytech/cap-ui-library/CapImage';
15
15
  import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
16
16
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
17
- import { ANDROID, IOS, ANDROID_DEVICE_NAME, IOS_DEVICE_NAME, VIBER_ACCOUNT_NAME } from '../constants';
17
+ import {
18
+ ANDROID,
19
+ IOS,
20
+ ANDROID_DEVICE_NAME,
21
+ IOS_DEVICE_NAME,
22
+ VIBER_ACCOUNT_NAME,
23
+ MEDIA_TYPE_CAROUSEL,
24
+ } from '../constants';
18
25
  import messages from '../messages';
19
26
  import videoPlay from '../../../assets/videoPlay.svg';
27
+ import ViberCarouselPreviewCards from './ViberCarouselPreviewCards';
20
28
 
21
29
  // Import device mockup images (same as SMS)
22
30
  const smsMobileAndroid = require('../../../assets/Android.png');
23
31
  const smsMobileIos = require('../../../assets/iOS.png');
24
32
 
33
+ const getTrimmedText = (value = '') => (value ?? '').trim();
34
+ const hasTrimmedText = (value = '') => Boolean(getTrimmedText(value));
35
+
25
36
  const ViberPreviewContent = ({
26
37
  content,
27
38
  device,
@@ -43,7 +54,35 @@ const ViberPreviewContent = ({
43
54
  imageURL = '',
44
55
  videoParams = {},
45
56
  buttonText = '',
57
+ type = '',
58
+ cards = [],
59
+ showCarouselEditorPreview = false,
46
60
  } = viberContent;
61
+ const hasCarouselContent = type === MEDIA_TYPE_CAROUSEL;
62
+
63
+ const cardHasMeaningfulContent = (card) => {
64
+ if (!card || typeof card !== 'object') return false;
65
+ return (
66
+ hasTrimmedText(card?.text) ||
67
+ hasTrimmedText(card?.mediaUrl) ||
68
+ (card.buttons ?? []).some(button => hasTrimmedText(button?.title))
69
+ );
70
+ };
71
+
72
+ const hasMeaningfulCarousel =
73
+ hasCarouselContent &&
74
+ Array.isArray(cards) &&
75
+ cards.length > 0 &&
76
+ cards.some(cardHasMeaningfulContent);
77
+
78
+ const showCarouselInPreview =
79
+ hasCarouselContent && (Boolean(showCarouselEditorPreview) || hasMeaningfulCarousel);
80
+
81
+ const previewCarouselCards =
82
+ hasCarouselContent && Array.isArray(cards) && cards.length ? cards : hasCarouselContent ? [{}] : [];
83
+
84
+ const trimmedMessageContent = getTrimmedText(messageContent);
85
+ const trimmedButtonText = getTrimmedText(buttonText);
47
86
 
48
87
  // Get account name (first letter for icon)
49
88
  const accountIcon = (accountName || brandName || 'V')[0]?.toUpperCase();
@@ -99,8 +138,15 @@ const ViberPreviewContent = ({
99
138
  );
100
139
  }
101
140
 
102
- // Check if there's any content to display
103
- const hasContent = messageContent || imageURL || videoParams?.viberVideoSrc || buttonText;
141
+ // Check if there's any content to display (whitespace-only strings do not count)
142
+ const hasContent = Boolean(
143
+ trimmedMessageContent
144
+ || hasTrimmedText(imageURL)
145
+ || videoParams?.viberVideoSrc
146
+ || trimmedButtonText
147
+ || hasMeaningfulCarousel
148
+ || (hasCarouselContent && showCarouselEditorPreview)
149
+ );
104
150
 
105
151
  // Render normal Viber preview
106
152
  return (
@@ -125,23 +171,34 @@ const ViberPreviewContent = ({
125
171
 
126
172
  {/* Viber Message Container */}
127
173
  {hasContent && (
128
- <CapRow className={`viber-message-container ${device !== ANDROID ? 'viber-message-container-ios' : ''}`}>
174
+ <CapRow className={`viber-message-container ${showCarouselInPreview ? 'viber-message-container-carousel' : ''} ${device !== ANDROID ? 'viber-message-container-ios' : ''}`}>
129
175
  {/* Brand Name Display (from TemplatePreview line 1136) */}
130
- <CapRow type="flex" align="middle" className="msg-container viber-preview">
176
+ <CapRow
177
+ type="flex"
178
+ align="middle"
179
+ fullWidth={showCarouselInPreview}
180
+ className={`msg-container viber-preview ${showCarouselInPreview ? 'viber-preview-carousel' : ''}`}
181
+ >
131
182
  {/* Account Icon (from TemplatePreview line 1146-1160) */}
132
- <CapRow type='flex' className="viber-account-icon">
133
- {accountIcon}
134
- </CapRow>
183
+ {!hasCarouselContent && (
184
+ <CapRow type="flex" className="viber-account-icon">
185
+ {accountIcon}
186
+ </CapRow>
187
+ )}
135
188
 
136
189
  {/* Message Bubble (from TemplatePreview line 1161-1223) */}
137
- <CapRow useLegacy className="message-pop align-left viber-message-pop">
190
+ <CapRow
191
+ useLegacy
192
+ fullWidth={showCarouselInPreview}
193
+ className={`message-pop align-left viber-message-pop ${showCarouselInPreview ? 'viber-message-pop-carousel' : ''}`}
194
+ >
138
195
  {/* Text Viber preview */}
139
- {messageContent && (
196
+ {trimmedMessageContent && !hasCarouselContent && (
140
197
  <CapLabel type="label15" className="viber-message-text">{messageContent}</CapLabel>
141
198
  )}
142
199
 
143
200
  {/* Image Viber preview */}
144
- {imageURL && (
201
+ {hasTrimmedText(imageURL) && (
145
202
  <CapImage
146
203
  src={imageURL}
147
204
  className="viber-image-preview"
@@ -171,16 +228,42 @@ const ViberPreviewContent = ({
171
228
  )}
172
229
 
173
230
  {/* Button Viber preview */}
174
- {buttonText && (
231
+ {trimmedButtonText && (
175
232
  <CapLabel className="viber-button-base">
176
233
  <p className="viber-button-card-text">
177
234
  {buttonText}
178
235
  </p>
179
236
  </CapLabel>
180
237
  )}
181
- <CapLabel type="label1" className="viber-timestamp">
182
- {timestamp}
183
- </CapLabel>
238
+ {/* Carousel Viber preview */}
239
+ {showCarouselInPreview && (
240
+ <CapRow type="flex-column" fullWidth className="viber-carousel-preview">
241
+ <CapRow type="flex-column" className="viber-carousel-message-pop">
242
+ {trimmedMessageContent ? (
243
+ <CapLabel
244
+ type="label15"
245
+ className="message-pop-item align-left viber-message-text viber-carousel-message-box-text"
246
+ >
247
+ {messageContent}
248
+ </CapLabel>
249
+ ) : (
250
+ <CapRow className="viber-carousel-message-box-placeholder" />
251
+ )}
252
+ <CapLabel type="label1" className="viber-carousel-message-timestamp">
253
+ {timestamp}
254
+ </CapLabel>
255
+ </CapRow>
256
+
257
+ <CapRow type="flex-column" fullWidth className="viber-carousel-cards-pop">
258
+ <ViberCarouselPreviewCards cards={previewCarouselCards} />
259
+ </CapRow>
260
+ </CapRow>
261
+ )}
262
+ {!showCarouselInPreview && (
263
+ <CapLabel type="label1" className="viber-timestamp">
264
+ {timestamp}
265
+ </CapLabel>
266
+ )}
184
267
  <CapRow useLegacy className="empty-placeholder" />
185
268
  </CapRow>
186
269
 
@@ -214,6 +297,16 @@ ViberPreviewContent.propTypes = {
214
297
  viberVideoPreviewImg: PropTypes.string,
215
298
  }),
216
299
  buttonText: PropTypes.string,
300
+ type: PropTypes.string,
301
+ cards: PropTypes.arrayOf(PropTypes.shape({
302
+ text: PropTypes.string,
303
+ mediaUrl: PropTypes.string,
304
+ buttons: PropTypes.arrayOf(PropTypes.shape({
305
+ title: PropTypes.string,
306
+ action: PropTypes.string,
307
+ })),
308
+ })),
309
+ showCarouselEditorPreview: PropTypes.bool,
217
310
  }),
218
311
  }),
219
312
  device: PropTypes.oneOf([ANDROID, IOS]),