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

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 (136) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapTagList/index.js +10 -0
  15. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +213 -21
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  22. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +0 -17
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -146
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +138 -48
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -4
  29. package/v2Components/CommonTestAndPreview/index.js +691 -235
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +0 -159
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -256
  42. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -2
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -198
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +36 -26
  46. package/v2Components/FormBuilder/index.js +11 -6
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +956 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +38 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -31
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +15 -3
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/App/constants.js +0 -3
  71. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  72. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  73. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  74. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  75. package/v2Containers/CreativesContainer/constants.js +9 -0
  76. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  77. package/v2Containers/CreativesContainer/index.js +322 -103
  78. package/v2Containers/CreativesContainer/index.scss +51 -1
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  88. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  89. package/v2Containers/Rcs/constants.js +119 -10
  90. package/v2Containers/Rcs/index.js +2445 -813
  91. package/v2Containers/Rcs/index.scss +280 -8
  92. package/v2Containers/Rcs/messages.js +34 -3
  93. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  94. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  95. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  96. package/v2Containers/Rcs/tests/index.test.js +152 -121
  97. package/v2Containers/Rcs/tests/mockData.js +38 -0
  98. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  99. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  100. package/v2Containers/Rcs/utils.js +478 -11
  101. package/v2Containers/Sms/Create/index.js +106 -40
  102. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  103. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  104. package/v2Containers/SmsTrai/Create/index.js +9 -4
  105. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  106. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  107. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  108. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  109. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  110. package/v2Containers/SmsWrapper/index.js +37 -8
  111. package/v2Containers/TagList/index.js +6 -0
  112. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  113. package/v2Containers/Templates/_templates.scss +166 -9
  114. package/v2Containers/Templates/actions.js +11 -0
  115. package/v2Containers/Templates/constants.js +2 -0
  116. package/v2Containers/Templates/index.js +122 -120
  117. package/v2Containers/Templates/sagas.js +56 -12
  118. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  119. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  120. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  121. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  122. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  123. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  124. package/v2Containers/TemplatesV2/index.js +86 -23
  125. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  126. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  127. package/v2Containers/WebPush/Create/index.js +8 -91
  128. package/v2Containers/WebPush/Create/index.scss +0 -7
  129. package/v2Containers/Whatsapp/index.js +3 -20
  130. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  131. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +0 -169
  132. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +0 -522
  133. package/v2Containers/App/tests/constants.test.js +0 -61
  134. package/v2Containers/Templates/tests/webpush.test.js +0 -375
  135. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +0 -338
  136. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +0 -325
@@ -9,29 +9,91 @@
9
9
 
10
10
  import get from 'lodash/get';
11
11
  import { CHANNELS } from '../../constants';
12
+ import { RCS_CONTACT_INFO_GSM_TYPE_KEYS } from '../constants';
12
13
 
14
+ /** Match API contactInfo.type (some envs use different casing). */
15
+ const typeMatches = (itemType, expectedKey) => {
16
+ if (!itemType || !expectedKey) return false;
17
+ const normalizedItemType = String(itemType).toLowerCase().replace(/-/g, '_');
18
+ const normalizedExpectedType = String(expectedKey).toLowerCase().replace(/-/g, '_');
19
+ return normalizedItemType === normalizedExpectedType;
20
+ };
21
+
22
+ /**
23
+ * Include row unless explicitly invalid — many APIs omit `valid`, which would
24
+ * otherwise filter out every sender (RCS/SMS lists empty in UI).
25
+ */
13
26
  const getSenderOptions = (contactInfo, key) =>
14
27
  (contactInfo || [])
15
- .filter((contactInfoItem) => contactInfoItem.type === key && contactInfoItem.valid)
28
+ .filter((contactInfoItem) => typeMatches(contactInfoItem.type, key) && contactInfoItem.valid !== false)
16
29
  .sort((contact) => (contact.default ? -1 : 0));
17
30
 
31
+ /** RCS APIs may label senders under several `contactInfo.type` keys — see `RCS_CONTACT_INFO_GSM_TYPE_KEYS`. */
32
+ const getRcsLikeGsmSenders = (contactInfo) => {
33
+ const seen = new Set();
34
+ const mergedSenderRows = [];
35
+ RCS_CONTACT_INFO_GSM_TYPE_KEYS.forEach((key) => {
36
+ getSenderOptions(contactInfo, key).forEach((row) => {
37
+ const senderValue = row?.value;
38
+ // Skip empty/whitespace sender values to avoid polluting the RCS sender dropdown.
39
+ if (senderValue == null || String(senderValue).trim() === '' || seen.has(senderValue)) return;
40
+ seen.add(senderValue);
41
+ mergedSenderRows.push(row);
42
+ });
43
+ });
44
+ return mergedSenderRows.sort((a, b) => (a.default ? -1 : 0) - (b.default ? -1 : 0));
45
+ };
46
+
47
+ /**
48
+ * Campaigns domainProperties may return { entity } or { result: { entity } } / { data: { entity } }.
49
+ */
50
+ function unwrapEntity(response) {
51
+ if (!response || typeof response !== 'object') return null;
52
+ if (response.entity != null) return response.entity;
53
+ const nested = get(response, 'result.entity') ?? get(response, 'data.entity');
54
+ if (nested != null) return nested;
55
+ if (response.entity === null) return null;
56
+ return response;
57
+ }
58
+
18
59
  /**
19
60
  * Parse raw API response for one channel into { domains: [...] }
20
- * @param {string} channel - SMS | EMAIL | WHATSAPP
61
+ * @param {string} channel - SMS | EMAIL | WHATSAPP | RCS
21
62
  * @param {Object} response - API response (e.g. { entity: { SMS: [...] } } or { entity: [...] })
22
63
  * @returns {{ domains: Array }} - domains array compatible with campaigns DeliverySettingsV2
23
64
  */
24
65
  export function parseSenderDetailsResponse(channel, response) {
25
- const entity = get(response, 'entity', response);
26
- if (!entity) {
66
+ const entity = unwrapEntity(response);
67
+ if (entity == null || entity === '') {
27
68
  return { domains: [] };
28
69
  }
29
70
 
30
- // Single-channel API: entity may be { [channel]: [...] } or direct array
31
- let channelSenderDetails = get(entity, channel, null);
71
+ // Normalize once: entity keys may be any casing (SMS/sms/rcs) but downstream branches use CHANNELS.* (uppercase).
72
+ const normalizedChannel =
73
+ channel == null || String(channel).trim() === ''
74
+ ? ''
75
+ : String(channel).trim().toUpperCase();
76
+
77
+ // Single-channel API: entity may be { [channel]: [...] } or direct array.
78
+ // Some responses use different casing (e.g. rcs vs RCS) or a single domain object instead of an array.
79
+ let channelSenderDetails =
80
+ get(entity, channel, null)
81
+ ?? (normalizedChannel ? get(entity, normalizedChannel, null) : null)
82
+ ?? (normalizedChannel ? get(entity, normalizedChannel.toLowerCase(), null) : null);
83
+
32
84
  if (channelSenderDetails == null && Array.isArray(entity)) {
33
85
  channelSenderDetails = entity;
34
86
  }
87
+
88
+ if (channelSenderDetails != null && !Array.isArray(channelSenderDetails)) {
89
+ const single = channelSenderDetails;
90
+ if (single && typeof single === 'object' && single.domainProperties) {
91
+ channelSenderDetails = [single];
92
+ } else {
93
+ return { domains: [] };
94
+ }
95
+ }
96
+
35
97
  if (!Array.isArray(channelSenderDetails)) {
36
98
  return { domains: [] };
37
99
  }
@@ -42,6 +104,7 @@ export function parseSenderDetailsResponse(channel, response) {
42
104
  const { id: dgmId, priority, domainProperties = {} } = element;
43
105
  const {
44
106
  domainName,
107
+ hostName,
45
108
  id = -1,
46
109
  contactInfo,
47
110
  connectionProperties: {
@@ -51,21 +114,25 @@ export function parseSenderDetailsResponse(channel, response) {
51
114
  } = {},
52
115
  } = domainProperties;
53
116
 
117
+ const resolvedDomainName = domainName || hostName;
118
+
54
119
  const domain = {
55
120
  dgmId,
56
- domainName,
121
+ domainName: resolvedDomainName,
57
122
  domainId: id,
58
123
  priority: priority != null ? priority : 0,
59
124
  };
60
125
 
61
- if ([CHANNELS.SMS, CHANNELS.WHATSAPP].includes(channel)) {
126
+ if ([CHANNELS.SMS, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(normalizedChannel)) {
62
127
  domain.cdmaSenders = getSenderOptions(contactInfo, 'cdma_sender_id');
63
- domain.gsmSenders = getSenderOptions(contactInfo, 'gsm_sender_id');
128
+ domain.gsmSenders = normalizedChannel === CHANNELS.RCS
129
+ ? getRcsLikeGsmSenders(contactInfo)
130
+ : getSenderOptions(contactInfo, 'gsm_sender_id');
64
131
  domain.sourceAccountIdentifier =
65
132
  sourceAccountIdentifier || wabaId || userid || '';
66
133
  }
67
134
 
68
- if (channel === CHANNELS.EMAIL) {
135
+ if (normalizedChannel === CHANNELS.EMAIL) {
69
136
  domain.emailSenders = getSenderOptions(contactInfo, 'sender_id');
70
137
  domain.emailRepliers = getSenderOptions(contactInfo, 'reply_to_id');
71
138
  }
@@ -76,7 +143,8 @@ export function parseSenderDetailsResponse(channel, response) {
76
143
  // Sort by priority and dedupe by domainName (match campaigns behaviour)
77
144
  domains.sort((a, b) => (a.priority || 0) - (b.priority || 0));
78
145
  const deduped = domains.reduce((acc, domain) => {
79
- if (!acc.find((d) => d.domainName === domain.domainName)) {
146
+ const key = domain.domainName ?? domain.domainId;
147
+ if (!acc.find((existing) => (existing.domainName ?? existing.domainId) === key)) {
80
148
  acc.push(domain);
81
149
  }
82
150
  return acc;
@@ -6,12 +6,13 @@ import CapButton from '@capillarytech/cap-ui-library/CapButton';
6
6
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
7
7
  import CapStepsAccordian from '@capillarytech/cap-ui-library/CapStepsAccordian';
8
8
  import CapTreeSelect from '@capillarytech/cap-ui-library/CapTreeSelect';
9
+ import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
9
10
  import isEmpty from 'lodash/isEmpty';
10
11
  import messages from './messages';
11
12
  import DeliverySettings from './DeliverySettings';
12
13
  import { CHANNELS } from './constants';
13
14
 
14
- const CHANNELS_WITH_DELIVERY_SETTINGS = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP];
15
+ const CHANNELS_WITH_DELIVERY_SETTINGS = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
15
16
 
16
17
  const SendTestMessage = ({
17
18
  isFetchingTestCustomers,
@@ -28,12 +29,13 @@ const SendTestMessage = ({
28
29
  searchValue,
29
30
  setSearchValue,
30
31
  deliverySettings,
31
- senderDetailsOptions,
32
+ senderDetailsByChannel = {},
32
33
  wecrmAccounts,
33
34
  onSaveDeliverySettings,
34
35
  isLoadingSenderDetails,
35
36
  smsTraiDltEnabled,
36
37
  registeredSenderIds,
38
+ isChannelSmsFallbackPreviewEnabled,
37
39
  }) => {
38
40
  const addCustomerContent = renderAddTestCustomerButton ? renderAddTestCustomerButton() : null;
39
41
  return (
@@ -77,13 +79,14 @@ const SendTestMessage = ({
77
79
  <DeliverySettings
78
80
  channel={channel}
79
81
  deliverySettings={deliverySettings || {}}
80
- senderDetailsOptions={senderDetailsOptions}
82
+ senderDetailsByChannel={senderDetailsByChannel}
81
83
  wecrmAccounts={wecrmAccounts || []}
82
84
  onSaveDeliverySettings={onSaveDeliverySettings}
83
85
  isLoadingSenderDetails={isLoadingSenderDetails}
84
86
  formatMessage={formatMessage}
85
87
  smsTraiDltEnabled={smsTraiDltEnabled}
86
88
  registeredSenderIds={registeredSenderIds}
89
+ isChannelSmsFallbackPreviewEnabled={isChannelSmsFallbackPreviewEnabled}
87
90
  whatsappAccountFromForm={
88
91
  channel === CHANNELS.WHATSAPP && formData?.accountName
89
92
  ? { accountName: formData.accountName }
@@ -121,12 +124,13 @@ SendTestMessage.propTypes = {
121
124
  searchValue: PropTypes.string,
122
125
  setSearchValue: PropTypes.func,
123
126
  deliverySettings: PropTypes.object,
124
- senderDetailsOptions: PropTypes.array,
127
+ senderDetailsByChannel: PropTypes.object,
125
128
  wecrmAccounts: PropTypes.array,
126
129
  onSaveDeliverySettings: PropTypes.func,
127
130
  isLoadingSenderDetails: PropTypes.bool,
128
131
  smsTraiDltEnabled: PropTypes.bool,
129
132
  registeredSenderIds: PropTypes.array,
133
+ isChannelSmsFallbackPreviewEnabled: PropTypes.bool,
130
134
  };
131
135
 
132
136
  SendTestMessage.defaultProps = {
@@ -136,12 +140,13 @@ SendTestMessage.defaultProps = {
136
140
  searchValue: '',
137
141
  setSearchValue: () => {}, // no-op when not provided; required by TreeSelect when showSearch is true
138
142
  deliverySettings: {},
139
- senderDetailsOptions: [],
143
+ senderDetailsByChannel: {},
140
144
  wecrmAccounts: [],
141
145
  onSaveDeliverySettings: undefined,
142
146
  isLoadingSenderDetails: false,
143
147
  smsTraiDltEnabled: false,
144
148
  registeredSenderIds: [],
149
+ isChannelSmsFallbackPreviewEnabled: false,
145
150
  };
146
151
 
147
152
  export default SendTestMessage;
@@ -23,7 +23,6 @@ const PreviewHeader = ({
23
23
  showDeviceToggle,
24
24
  onDeviceChange,
25
25
  channel,
26
- setIsFullscreenOpen,
27
26
  }) => {
28
27
  // Determine if this is SMS, WhatsApp, RCS, InApp, MobilePush, or Viber channel (uses Android/iOS) or other channels (uses Desktop/Mobile)
29
28
  const isSmsChannel = channel === CHANNELS.SMS;
@@ -32,13 +31,8 @@ const PreviewHeader = ({
32
31
  const isInAppChannel = channel === CHANNELS.INAPP;
33
32
  const isMobilePushChannel = channel === CHANNELS.MOBILEPUSH;
34
33
  const isViberChannel = channel === CHANNELS.VIBER;
35
- const isWebPushChannel = channel === CHANNELS.WEBPUSH;
36
34
  const isAndroidIosToggle = isSmsChannel || isWhatsappChannel || isRcsChannel || isInAppChannel || isMobilePushChannel || isViberChannel;
37
35
 
38
- const handleOpenFullscreen = () => {
39
- setIsFullscreenOpen(true);
40
- };
41
-
42
36
  return (
43
37
  <CapRow className="preview-chrome">
44
38
  <div className="preview-header">
@@ -86,13 +80,6 @@ const PreviewHeader = ({
86
80
  )}
87
81
  </CapRow>
88
82
  )}
89
- {isWebPushChannel && (
90
- <CapIcon
91
- type="expander"
92
- className={device === MOBILE ? ACTIVE : ''}
93
- onClick={() => handleOpenFullscreen()}
94
- />
95
- )}
96
83
  </div>
97
84
  </CapRow>
98
85
  );
@@ -108,8 +95,6 @@ PreviewHeader.propTypes = {
108
95
  showDeviceToggle: PropTypes.bool,
109
96
  onDeviceChange: PropTypes.func,
110
97
  channel: PropTypes.string,
111
- isFullscreenOpen: PropTypes.bool,
112
- setIsFullscreenOpen: PropTypes.func,
113
98
  };
114
99
 
115
100
  PreviewHeader.defaultProps = {
@@ -118,8 +103,6 @@ PreviewHeader.defaultProps = {
118
103
  showDeviceToggle: false,
119
104
  onDeviceChange: () => {},
120
105
  channel: null,
121
- isFullscreenOpen: () => {},
122
- setIsFullscreenOpen: () => false,
123
106
  };
124
107
 
125
108
  export default PreviewHeader;
@@ -25,6 +25,7 @@ import {
25
25
  TIME_FORMAT_MINUTE_PADDING_THRESHOLD,
26
26
  TIME_FORMAT_AM,
27
27
  TIME_FORMAT_PM,
28
+ MEDIA_TYPE_VIDEO,
28
29
  } from '../constants';
29
30
  import messages from '../messages';
30
31
  import { RCS_BUTTON_TYPES } from '../../../v2Containers/Rcs/constants';
@@ -50,41 +51,57 @@ const RcsPreviewContent = ({
50
51
  const deviceName = device === ANDROID ? ANDROID_DEVICE_NAME : IOS_DEVICE_NAME;
51
52
 
52
53
  // Render RCS suggestions (Quick Reply, CTA, Phone Number)
53
- const renderRcsSuggestionsPreview = () => {
54
+ // options.isCarousel: stacked bars below card body with gap (Figma), no dividers between CTAs
55
+ const renderRcsSuggestionsPreview = (suggestionsArg, options = {}) => {
56
+ const { isCarousel = false } = options;
54
57
  const renderArray = [];
55
- const { suggestions = [] } = content || {};
58
+ const suggestions = Array.isArray(suggestionsArg)
59
+ ? suggestionsArg
60
+ : (content?.suggestions || []);
56
61
 
57
62
  if (suggestions.length === 0) {
58
63
  return null;
59
64
  }
60
65
 
66
+ const ctaClass = isCarousel ? 'rcs-cta-preview rcs-cta-preview--carousel-bar' : 'rcs-cta-preview';
67
+
61
68
  suggestions.forEach((suggestion) => {
62
69
  const { type, text } = suggestion || {};
63
70
  const suggestionKey = `${type}-${text || Math.random()}`;
64
71
 
65
- if (renderArray.length > 0) {
72
+ if (renderArray.length > 0 && !isCarousel) {
66
73
  renderArray.push(<CapDivider key={`divider-${suggestionKey}`} className="whatsapp-divider" />);
67
74
  }
68
75
 
69
76
  if (type === RCS_BUTTON_TYPES.QUICK_REPLY) {
70
77
  renderArray.push(
71
- <CapLabel key={suggestionKey} type="label21" className="rcs-cta-preview">
78
+ <CapLabel key={suggestionKey} type="label21" className={ctaClass}>
72
79
  <CapIcon type="small-link" size="xs" />
73
80
  {text}
74
81
  </CapLabel>
75
82
  );
76
83
  } else if (type === RCS_BUTTON_TYPES.CTA) {
77
84
  renderArray.push(
78
- <CapLabel key={suggestionKey} type="label21" className="rcs-cta-preview">
85
+ <CapLabel key={suggestionKey} type="label21" className={ctaClass}>
79
86
  <CapIcon type="launch" size="xs" />
80
87
  {text}
81
88
  </CapLabel>
82
89
  );
83
90
  } else if (type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
91
+ // Carousel: label text then phone icon (matches device-style RCS preview)
84
92
  renderArray.push(
85
- <CapLabel key={suggestionKey} type="label21" className="rcs-cta-preview">
86
- <CapIcon type="call" size="xs" />
87
- {text}
93
+ <CapLabel key={suggestionKey} type="label21" className={ctaClass}>
94
+ {isCarousel ? (
95
+ <>
96
+ {text}
97
+ <CapIcon type="call" size="xs" className="rcs-cta-preview-phone-icon-end" />
98
+ </>
99
+ ) : (
100
+ <>
101
+ <CapIcon type="call" size="xs" />
102
+ {text}
103
+ </>
104
+ )}
88
105
  </CapLabel>
89
106
  );
90
107
  }
@@ -93,6 +110,104 @@ const RcsPreviewContent = ({
93
110
  return renderArray?.length > 0 ? renderArray : null;
94
111
  };
95
112
 
113
+ // Carousel media box: aspect matches RCS editor card size (SHORT/MEDIUM × SMALL/MEDIUM).
114
+ const getCarouselMediaAspectStyle = (isVideo) => {
115
+ const d = content?.carouselPreviewDimensions;
116
+ if (!d) return undefined;
117
+ if (isVideo) {
118
+ const { videoThumbWidth, videoThumbHeight } = d;
119
+ if (!videoThumbWidth || !videoThumbHeight) return undefined;
120
+ return { aspectRatio: `${videoThumbWidth} / ${videoThumbHeight}` };
121
+ }
122
+ const { imageWidth, imageHeight } = d;
123
+ if (!imageWidth || !imageHeight) return undefined;
124
+ return { aspectRatio: `${imageWidth} / ${imageHeight}` };
125
+ };
126
+
127
+ // Carousel preview (WhatsApp-style static horizontal scroll).
128
+ // Caller passes a non-empty array only (see hasCarousel); avoids duplicate guards that Sonar
129
+ // flagged as partially covered because inner checks were unreachable when hasCarousel was true.
130
+ const renderCarouselPreviewContent = (carouselCards) => (
131
+ <CapRow className="msg-container-carousel">
132
+ <CapRow className="scroll-container">
133
+ {carouselCards.map((card, idx) => {
134
+ const key = `rcs-carousel-${idx}-${card?.bodyText || card?.imageSrc || card?.videoPreviewImg || ''}`;
135
+ const isVideo =
136
+ (card?.mediaType || '').toLowerCase() === String(MEDIA_TYPE_VIDEO).toLowerCase();
137
+ const cardSuggestions = Array.isArray(card?.suggestions) ? card.suggestions : [];
138
+ const suggestionsNode = cardSuggestions.length > 0
139
+ ? renderRcsSuggestionsPreview(cardSuggestions, { isCarousel: true })
140
+ : null;
141
+ const titleTrimmed = String(card?.title ?? '').trim();
142
+ const bodyTrimmed = String(card?.bodyText ?? '').trim();
143
+ const titleShown = titleTrimmed || formatMessage(messages.rcsCarouselPreviewTitlePlaceholder);
144
+ const bodyShown = bodyTrimmed || formatMessage(messages.rcsCarouselPreviewBodyPlaceholder);
145
+ return (
146
+ <CapRow
147
+ key={key}
148
+ className="message-pop align-left message-pop-carousel"
149
+ >
150
+ <CapRow className="whatsapp-content">
151
+ {!isVideo && (
152
+ <CapRow
153
+ className="whatsapp-image rcs-carousel-media-wrap"
154
+ style={getCarouselMediaAspectStyle(false)}
155
+ >
156
+ <CapImage
157
+ src={card?.imageSrc ? card.imageSrc : rcsImageEmptyPreview}
158
+ className="rcs-carousel-img"
159
+ alt={formatMessage(messages.previewGenerated)}
160
+ />
161
+ </CapRow>
162
+ )}
163
+ {isVideo && (
164
+ <CapTooltip title={formatMessage(messages.videoPreviewTooltip)}>
165
+ <CapRow className="video-preview">
166
+ <CapRow
167
+ className="whatsapp-image rcs-carousel-media-wrap"
168
+ style={getCarouselMediaAspectStyle(true)}
169
+ >
170
+ <CapImage
171
+ src={card?.videoPreviewImg ? card.videoPreviewImg : rcsVideoEmptyPreview}
172
+ className="rcs-carousel-img"
173
+ alt={formatMessage(messages.previewGenerated)}
174
+ />
175
+ </CapRow>
176
+ <CapRow className="icon-position">
177
+ <CapImage className="video-icon" src={videoPlay} alt="Play" />
178
+ </CapRow>
179
+ </CapRow>
180
+ </CapTooltip>
181
+ )}
182
+
183
+ <CapRow className="carousel-content">
184
+ <CapLabel
185
+ type={card.titleLabelType || 'label1'}
186
+ className={`carousel-title${titleTrimmed ? '' : ' rcs-carousel-field-placeholder'}`}
187
+ >
188
+ {titleShown}
189
+ </CapLabel>
190
+ <CapLabel
191
+ type={card.bodyLabelType || 'label2'}
192
+ className={`carousel-message${bodyTrimmed ? '' : ' rcs-carousel-field-placeholder'}`}
193
+ >
194
+ {bodyShown}
195
+ </CapLabel>
196
+ </CapRow>
197
+
198
+ {!!suggestionsNode && (
199
+ <CapRow className="rcs-carousel-cta-stack" gutter={0}>
200
+ {suggestionsNode}
201
+ </CapRow>
202
+ )}
203
+ </CapRow>
204
+ </CapRow>
205
+ );
206
+ })}
207
+ </CapRow>
208
+ </CapRow>
209
+ );
210
+
96
211
  // Render text preview content
97
212
  const renderTextPreviewContent = () => {
98
213
  const {
@@ -226,11 +341,22 @@ const RcsPreviewContent = ({
226
341
  const timestamp = `${displayHours}:${displayMinutes} ${ampm}`;
227
342
 
228
343
  // Render normal RCS preview (same structure as SMS up to sms-message-container)
344
+ const carouselCardsNormalized = Array.isArray(content?.carouselData) ? content?.carouselData : [];
345
+ const hasCarousel = carouselCardsNormalized?.length > 0;
229
346
  const hasMedia = !!(content?.rcsImageSrc || content?.rcsVideoSrc);
230
- const contentToRender = hasMedia ? renderMediaPreviewContent() : renderTextPreviewContent();
347
+ let contentToRender;
348
+ if (hasCarousel) {
349
+ contentToRender = renderCarouselPreviewContent(carouselCardsNormalized);
350
+ } else if (hasMedia) {
351
+ contentToRender = renderMediaPreviewContent();
352
+ } else {
353
+ contentToRender = renderTextPreviewContent();
354
+ }
231
355
 
232
356
  return (
233
- <CapRow className="sms-device-container">
357
+ <CapRow
358
+ className={`sms-device-container rcs-device-container ${hasCarousel ? 'rcs-device-container-carousel' : ''}`}
359
+ >
234
360
  {/* Device Background Image */}
235
361
  <CapImage
236
362
  className="sms-device-image"
@@ -239,7 +365,9 @@ const RcsPreviewContent = ({
239
365
  />
240
366
 
241
367
  {/* Content Overlay */}
242
- <CapRow className={`sms-content-overlay sms-content-overlay-${device} sms-preview sms-${device}`}>
368
+ <CapRow
369
+ className={`sms-content-overlay sms-content-overlay-${device} sms-preview sms-${device} rcs-content-overlay`}
370
+ >
243
371
  {/* Navigation Bar - Same as SMS */}
244
372
  <CapRow className={`sms-navigation-bar ${!showHeader ? 'sms-navigation-bar-no-header' : ''}`}>
245
373
  <CapIcon type="chevron-left" className="sms-back-arrow" />
@@ -255,8 +383,8 @@ const RcsPreviewContent = ({
255
383
  </CapRow>
256
384
 
257
385
  {/* Message Container - Same structure as SMS, only content inside is RCS-specific */}
258
- <CapRow className="sms-message-container">
259
- <CapRow className="sms-message-bubble">
386
+ <CapRow className={`sms-message-container ${hasCarousel ? 'rcs-message-container-carousel' : ''}`}>
387
+ <CapRow className={`sms-message-bubble ${hasCarousel ? 'rcs-message-bubble-carousel' : ''}`}>
260
388
  <CapRow className="sms-message-text">
261
389
  {/* RCS-specific content rendering */}
262
390
  <CapRow className="message-pop sms">
@@ -270,8 +398,10 @@ const RcsPreviewContent = ({
270
398
  </CapRow>
271
399
  </CapRow>
272
400
  </CapRow>
273
- );
274
- };
401
+ )
402
+ }
403
+
404
+
275
405
 
276
406
  RcsPreviewContent.propTypes = {
277
407
  content: PropTypes.shape({
@@ -282,6 +412,18 @@ RcsPreviewContent.propTypes = {
282
412
  rcsImageSrc: PropTypes.string,
283
413
  rcsVideoSrc: PropTypes.string,
284
414
  rcsThumbnailSrc: PropTypes.string,
415
+ carouselData: PropTypes.arrayOf(
416
+ PropTypes.shape({
417
+ titleLabelType: PropTypes.string,
418
+ bodyLabelType: PropTypes.string,
419
+ })
420
+ ),
421
+ carouselPreviewDimensions: PropTypes.shape({
422
+ imageWidth: PropTypes.number,
423
+ imageHeight: PropTypes.number,
424
+ videoThumbWidth: PropTypes.number,
425
+ videoThumbHeight: PropTypes.number,
426
+ }),
285
427
  suggestions: PropTypes.arrayOf(
286
428
  PropTypes.shape({
287
429
  type: PropTypes.string,