@capillarytech/creatives-library 9.0.14 → 9.0.15-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 (175) hide show
  1. package/constants/unified.js +32 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/common.js +8 -0
  6. package/utils/commonUtils.js +19 -1
  7. package/utils/rcsPayloadUtils.js +92 -0
  8. package/utils/templateVarUtils.js +201 -0
  9. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  10. package/utils/tests/templateVarUtils.test.js +204 -0
  11. package/v2Components/CapActionButton/constants.js +7 -0
  12. package/v2Components/CapActionButton/index.js +168 -109
  13. package/v2Components/CapActionButton/index.scss +156 -5
  14. package/v2Components/CapActionButton/messages.js +19 -3
  15. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  16. package/v2Components/CapImageUpload/index.js +2 -2
  17. package/v2Components/CapTagList/index.js +10 -0
  18. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  24. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +60 -11
  25. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +201 -23
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +355 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +761 -222
  32. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  35. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  36. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +284 -267
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  39. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  40. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  41. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  43. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  44. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  45. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  46. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/FormBuilder/Classic.js +4487 -0
  49. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +366 -0
  50. package/v2Components/FormBuilder/Functional/channels/registry.js +17 -0
  51. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +9 -0
  52. package/v2Components/FormBuilder/Functional/channels/sms/config.js +30 -0
  53. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +46 -0
  54. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +13 -0
  55. package/v2Components/FormBuilder/Functional/channels/sms/index.js +22 -0
  56. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +52 -0
  57. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +25 -0
  58. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +87 -0
  59. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +89 -0
  60. package/v2Components/FormBuilder/Functional/constants.js +42 -0
  61. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +38 -0
  62. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +85 -0
  63. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +81 -0
  64. package/v2Components/FormBuilder/Functional/core/store/selectors.js +30 -0
  65. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +91 -0
  66. package/v2Components/FormBuilder/Functional/index.js +26 -0
  67. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +59 -0
  68. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +31 -0
  69. package/v2Components/FormBuilder/Functional/layout/Section.js +116 -0
  70. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +258 -0
  71. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +21 -0
  72. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +65 -0
  73. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +97 -0
  74. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +192 -0
  75. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +129 -0
  76. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +132 -0
  77. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +40 -0
  78. package/v2Components/FormBuilder/Functional/tests/section.test.js +99 -0
  79. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +67 -0
  80. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +155 -0
  81. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +172 -0
  82. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +122 -0
  83. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +329 -0
  84. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +160 -0
  85. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +95 -0
  86. package/v2Components/FormBuilder/_formBuilder.scss +5 -0
  87. package/v2Components/FormBuilder/index.js +41 -4479
  88. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +114 -0
  89. package/v2Components/FormBuilder/tests/entryGate.test.js +106 -0
  90. package/v2Components/FormBuilder/tests/sms.characterization.test.js +336 -0
  91. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  92. package/v2Components/SmsFallback/constants.js +94 -0
  93. package/v2Components/SmsFallback/index.js +958 -0
  94. package/v2Components/SmsFallback/index.scss +266 -0
  95. package/v2Components/SmsFallback/messages.js +78 -0
  96. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  97. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  98. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  99. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  100. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  101. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  102. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  103. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  104. package/v2Components/TemplatePreview/_templatePreview.scss +41 -22
  105. package/v2Components/TemplatePreview/constants.js +2 -0
  106. package/v2Components/TemplatePreview/index.js +143 -31
  107. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  108. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  109. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  110. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  111. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  112. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  113. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  114. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  115. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  116. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  117. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  118. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  119. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  120. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  121. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  122. package/v2Containers/CreativesContainer/constants.js +11 -0
  123. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  124. package/v2Containers/CreativesContainer/index.js +323 -104
  125. package/v2Containers/CreativesContainer/index.scss +83 -1
  126. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  127. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  128. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  129. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  130. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +333 -90
  131. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  132. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  133. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  134. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  135. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  136. package/v2Containers/Rcs/constants.js +131 -11
  137. package/v2Containers/Rcs/index.js +2602 -807
  138. package/v2Containers/Rcs/index.scss +289 -8
  139. package/v2Containers/Rcs/messages.js +34 -3
  140. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +227 -0
  141. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +73535 -38537
  142. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -8
  143. package/v2Containers/Rcs/tests/index.test.js +147 -36
  144. package/v2Containers/Rcs/tests/mockData.js +38 -0
  145. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  146. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  147. package/v2Containers/Rcs/utils.js +478 -11
  148. package/v2Containers/Sms/Create/index.js +115 -48
  149. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  150. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  151. package/v2Containers/SmsTrai/Create/index.js +9 -4
  152. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  153. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  154. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  155. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  156. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  157. package/v2Containers/SmsWrapper/index.js +37 -8
  158. package/v2Containers/TagList/index.js +6 -0
  159. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  160. package/v2Containers/Templates/_templates.scss +171 -12
  161. package/v2Containers/Templates/actions.js +11 -0
  162. package/v2Containers/Templates/constants.js +2 -0
  163. package/v2Containers/Templates/index.js +125 -55
  164. package/v2Containers/Templates/sagas.js +57 -13
  165. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  166. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  167. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  168. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  169. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  170. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  171. package/v2Containers/TemplatesV2/index.js +86 -23
  172. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  173. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  174. package/v2Containers/Whatsapp/index.js +3 -20
  175. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -9,29 +9,72 @@
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';
13
+ import { typeMatches, unwrapEntity } from '../../utils';
12
14
 
15
+ /**
16
+ * Include row unless explicitly invalid — many APIs omit `valid`, which would
17
+ * otherwise filter out every sender (RCS/SMS lists empty in UI).
18
+ */
13
19
  const getSenderOptions = (contactInfo, key) =>
14
20
  (contactInfo || [])
15
- .filter((contactInfoItem) => contactInfoItem.type === key && contactInfoItem.valid)
21
+ .filter((contactInfoItem) => typeMatches(contactInfoItem.type, key) && contactInfoItem.valid !== false)
16
22
  .sort((contact) => (contact.default ? -1 : 0));
17
23
 
24
+ /** RCS APIs may label senders under several `contactInfo.type` keys — see `RCS_CONTACT_INFO_GSM_TYPE_KEYS`. */
25
+ const getRcsLikeGsmSenders = (contactInfo) => {
26
+ const seen = new Set();
27
+ const mergedSenderRows = [];
28
+ RCS_CONTACT_INFO_GSM_TYPE_KEYS.forEach((key) => {
29
+ getSenderOptions(contactInfo, key).forEach((row) => {
30
+ const senderValue = row?.value;
31
+ // Skip empty/whitespace sender values to avoid polluting the RCS sender dropdown.
32
+ if (senderValue == null || String(senderValue).trim() === '' || seen.has(senderValue)) return;
33
+ seen.add(senderValue);
34
+ mergedSenderRows.push(row);
35
+ });
36
+ });
37
+ return mergedSenderRows.sort((a, b) => (a.default ? -1 : 0) - (b.default ? -1 : 0));
38
+ };
39
+
18
40
  /**
19
41
  * Parse raw API response for one channel into { domains: [...] }
20
- * @param {string} channel - SMS | EMAIL | WHATSAPP
42
+ * @param {string} channel - SMS | EMAIL | WHATSAPP | RCS
21
43
  * @param {Object} response - API response (e.g. { entity: { SMS: [...] } } or { entity: [...] })
22
44
  * @returns {{ domains: Array }} - domains array compatible with campaigns DeliverySettingsV2
23
45
  */
24
46
  export function parseSenderDetailsResponse(channel, response) {
25
- const entity = get(response, 'entity', response);
26
- if (!entity) {
47
+ const entity = unwrapEntity(response);
48
+ if (entity == null || entity === '') {
27
49
  return { domains: [] };
28
50
  }
29
51
 
30
- // Single-channel API: entity may be { [channel]: [...] } or direct array
31
- let channelSenderDetails = get(entity, channel, null);
52
+ // Normalize once: entity keys may be any casing (SMS/sms/rcs) but downstream branches use CHANNELS.* (uppercase).
53
+ const normalizedChannel =
54
+ channel == null || String(channel).trim() === ''
55
+ ? ''
56
+ : String(channel).trim().toUpperCase();
57
+
58
+ // Single-channel API: entity may be { [channel]: [...] } or direct array.
59
+ // Some responses use different casing (e.g. rcs vs RCS) or a single domain object instead of an array.
60
+ let channelSenderDetails =
61
+ get(entity, channel, null)
62
+ ?? (normalizedChannel ? get(entity, normalizedChannel, null) : null)
63
+ ?? (normalizedChannel ? get(entity, normalizedChannel.toLowerCase(), null) : null);
64
+
32
65
  if (channelSenderDetails == null && Array.isArray(entity)) {
33
66
  channelSenderDetails = entity;
34
67
  }
68
+
69
+ if (channelSenderDetails != null && !Array.isArray(channelSenderDetails)) {
70
+ const single = channelSenderDetails;
71
+ if (single && typeof single === 'object' && single.domainProperties) {
72
+ channelSenderDetails = [single];
73
+ } else {
74
+ return { domains: [] };
75
+ }
76
+ }
77
+
35
78
  if (!Array.isArray(channelSenderDetails)) {
36
79
  return { domains: [] };
37
80
  }
@@ -42,6 +85,7 @@ export function parseSenderDetailsResponse(channel, response) {
42
85
  const { id: dgmId, priority, domainProperties = {} } = element;
43
86
  const {
44
87
  domainName,
88
+ hostName,
45
89
  id = -1,
46
90
  contactInfo,
47
91
  connectionProperties: {
@@ -51,21 +95,25 @@ export function parseSenderDetailsResponse(channel, response) {
51
95
  } = {},
52
96
  } = domainProperties;
53
97
 
98
+ const resolvedDomainName = domainName || hostName;
99
+
54
100
  const domain = {
55
101
  dgmId,
56
- domainName,
102
+ domainName: resolvedDomainName,
57
103
  domainId: id,
58
104
  priority: priority != null ? priority : 0,
59
105
  };
60
106
 
61
- if ([CHANNELS.SMS, CHANNELS.WHATSAPP].includes(channel)) {
107
+ if ([CHANNELS.SMS, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(normalizedChannel)) {
62
108
  domain.cdmaSenders = getSenderOptions(contactInfo, 'cdma_sender_id');
63
- domain.gsmSenders = getSenderOptions(contactInfo, 'gsm_sender_id');
109
+ domain.gsmSenders = normalizedChannel === CHANNELS.RCS
110
+ ? getRcsLikeGsmSenders(contactInfo)
111
+ : getSenderOptions(contactInfo, 'gsm_sender_id');
64
112
  domain.sourceAccountIdentifier =
65
113
  sourceAccountIdentifier || wabaId || userid || '';
66
114
  }
67
115
 
68
- if (channel === CHANNELS.EMAIL) {
116
+ if (normalizedChannel === CHANNELS.EMAIL) {
69
117
  domain.emailSenders = getSenderOptions(contactInfo, 'sender_id');
70
118
  domain.emailRepliers = getSenderOptions(contactInfo, 'reply_to_id');
71
119
  }
@@ -76,7 +124,8 @@ export function parseSenderDetailsResponse(channel, response) {
76
124
  // Sort by priority and dedupe by domainName (match campaigns behaviour)
77
125
  domains.sort((a, b) => (a.priority || 0) - (b.priority || 0));
78
126
  const deduped = domains.reduce((acc, domain) => {
79
- if (!acc.find((d) => d.domainName === domain.domainName)) {
127
+ const key = domain.domainName ?? domain.domainId;
128
+ if (!acc.find((existing) => (existing.domainName ?? existing.domainId) === key)) {
80
129
  acc.push(domain);
81
130
  }
82
131
  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;
@@ -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,108 @@ 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" type="flex" noWrap>
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
165
+ title={formatMessage(messages.videoPreviewTooltip)}
166
+ wrapperClassName="rcs-carousel-video-tooltip-wrapper"
167
+ triggerClassName="rcs-carousel-video-tooltip-trigger"
168
+ >
169
+ <CapRow className="video-preview">
170
+ <CapRow
171
+ className="whatsapp-image rcs-carousel-media-wrap"
172
+ style={getCarouselMediaAspectStyle(true)}
173
+ >
174
+ <CapImage
175
+ src={card?.videoPreviewImg ? card.videoPreviewImg : rcsVideoEmptyPreview}
176
+ className="rcs-carousel-img"
177
+ alt={formatMessage(messages.previewGenerated)}
178
+ />
179
+ </CapRow>
180
+ <CapRow className="icon-position">
181
+ <CapImage className="video-icon" src={videoPlay} alt="Play" />
182
+ </CapRow>
183
+ </CapRow>
184
+ </CapTooltip>
185
+ )}
186
+
187
+ <CapRow className="carousel-content">
188
+ <CapLabel
189
+ type={card.titleLabelType || 'label1'}
190
+ className={`carousel-title${titleTrimmed ? '' : ' rcs-carousel-field-placeholder'}`}
191
+ >
192
+ {titleShown}
193
+ </CapLabel>
194
+ <CapLabel
195
+ type={card.bodyLabelType || 'label2'}
196
+ className={`carousel-message${bodyTrimmed ? '' : ' rcs-carousel-field-placeholder'}`}
197
+ >
198
+ {bodyShown}
199
+ </CapLabel>
200
+ </CapRow>
201
+
202
+ {!!suggestionsNode && (
203
+ <CapRow className="rcs-carousel-cta-stack" gutter={0}>
204
+ {suggestionsNode}
205
+ </CapRow>
206
+ )}
207
+ </CapRow>
208
+ </CapRow>
209
+ );
210
+ })}
211
+ </CapRow>
212
+ </CapRow>
213
+ );
214
+
96
215
  // Render text preview content
97
216
  const renderTextPreviewContent = () => {
98
217
  const {
@@ -226,11 +345,39 @@ const RcsPreviewContent = ({
226
345
  const timestamp = `${displayHours}:${displayMinutes} ${ampm}`;
227
346
 
228
347
  // Render normal RCS preview (same structure as SMS up to sms-message-container)
348
+ const carouselCardsNormalized = Array.isArray(content?.carouselData) ? content?.carouselData : [];
349
+ const hasCarousel = carouselCardsNormalized?.length > 0;
229
350
  const hasMedia = !!(content?.rcsImageSrc || content?.rcsVideoSrc);
230
- const contentToRender = hasMedia ? renderMediaPreviewContent() : renderTextPreviewContent();
351
+ let contentToRender;
352
+ if (hasCarousel) {
353
+ contentToRender = renderCarouselPreviewContent(carouselCardsNormalized);
354
+ } else if (hasMedia) {
355
+ contentToRender = renderMediaPreviewContent();
356
+ } else {
357
+ contentToRender = renderTextPreviewContent();
358
+ }
359
+
360
+ const carouselPanelStyle = hasCarousel ? {
361
+ position: 'absolute',
362
+ left: '50%',
363
+ transform: 'translateX(-50%)',
364
+ width: '100%',
365
+ maxWidth: '26.786rem',
366
+ boxSizing: 'border-box',
367
+ top: device === ANDROID ? '3.1rem' : '4.143rem',
368
+ height: device === ANDROID ? 'calc(100% - 3.8rem)' : device === IOS ? 'calc(100% - 5rem)' : 'calc(100% - 4.5rem)',
369
+ display: 'flex',
370
+ flexDirection: 'column',
371
+ paddingTop: '5.857rem',
372
+ paddingBottom: '1.142rem',
373
+ } : undefined;
231
374
 
232
375
  return (
233
- <CapRow type='flex' align='middle' justify='center' className="sms-device-container">
376
+ <CapRow
377
+ type='flex' align='middle' justify='center'
378
+ className={`sms-device-container rcs-device-container ${hasCarousel ? 'rcs-device-container-carousel' : ''}`}
379
+ style={{ position: 'relative' }}
380
+ >
234
381
  {/* Device Background Image */}
235
382
  <CapImage
236
383
  className="sms-device-image"
@@ -238,8 +385,10 @@ const RcsPreviewContent = ({
238
385
  alt={`${deviceName} Device`}
239
386
  />
240
387
 
241
- {/* Content Overlay */}
242
- <CapRow className={`sms-content-overlay sms-content-overlay-${device} sms-preview sms-${device}`}>
388
+ {/* Content Overlay - nav bar + date separator always; message content only for non-carousel */}
389
+ <CapRow
390
+ className={`sms-content-overlay sms-content-overlay-${device} sms-preview sms-${device} rcs-content-overlay`}
391
+ >
243
392
  {/* Navigation Bar - Same as SMS */}
244
393
  <CapRow type='flex' className={`sms-navigation-bar ${!showHeader ? 'sms-navigation-bar-no-header' : ''}`}>
245
394
  <CapIcon type="chevron-left" className="sms-back-arrow" />
@@ -254,24 +403,41 @@ const RcsPreviewContent = ({
254
403
  <FormattedMessage {...messages.timeAgo} defaultMessage="Today" />
255
404
  </CapRow>
256
405
 
257
- {/* Message Container - Same structure as SMS, only content inside is RCS-specific */}
258
- <CapRow className="sms-message-container">
259
- <CapRow useLegacy className="sms-message-bubble">
260
- <CapRow useLegacy className="sms-message-text">
261
- {/* RCS-specific content rendering */}
262
- <CapRow useLegacy className="message-pop sms">
263
- {contentToRender}
406
+ {/* Message Container - non-carousel only; carousel is rendered outside this overflow-x:hidden ancestor */}
407
+ {!hasCarousel && (
408
+ <CapRow className="sms-message-container">
409
+ <CapRow useLegacy className="sms-message-bubble">
410
+ <CapRow useLegacy className="sms-message-text">
411
+ <CapRow useLegacy className="message-pop sms">
412
+ {contentToRender}
413
+ </CapRow>
264
414
  </CapRow>
265
415
  </CapRow>
416
+ <CapRow useLegacy className="sms-timestamp">
417
+ {timestamp}
418
+ </CapRow>
266
419
  </CapRow>
267
- <CapRow useLegacy className="sms-timestamp">
420
+ )}
421
+ </CapRow>
422
+
423
+ {/* Carousel panel: sibling of overlay, no overflow-x:hidden ancestor → scroll-container
424
+ can receive horizontal scroll events. Positioned absolutely over the device image. */}
425
+ {hasCarousel && (
426
+ <CapRow
427
+ className={`rcs-carousel-panel rcs-carousel-panel-${device}`}
428
+ style={carouselPanelStyle}
429
+ >
430
+ {renderCarouselPreviewContent(carouselCardsNormalized)}
431
+ <CapRow useLegacy className="sms-timestamp rcs-carousel-timestamp">
268
432
  {timestamp}
269
433
  </CapRow>
270
434
  </CapRow>
271
- </CapRow>
435
+ )}
272
436
  </CapRow>
273
- );
274
- };
437
+ )
438
+ }
439
+
440
+
275
441
 
276
442
  RcsPreviewContent.propTypes = {
277
443
  content: PropTypes.shape({
@@ -282,6 +448,18 @@ RcsPreviewContent.propTypes = {
282
448
  rcsImageSrc: PropTypes.string,
283
449
  rcsVideoSrc: PropTypes.string,
284
450
  rcsThumbnailSrc: PropTypes.string,
451
+ carouselData: PropTypes.arrayOf(
452
+ PropTypes.shape({
453
+ titleLabelType: PropTypes.string,
454
+ bodyLabelType: PropTypes.string,
455
+ })
456
+ ),
457
+ carouselPreviewDimensions: PropTypes.shape({
458
+ imageWidth: PropTypes.number,
459
+ imageHeight: PropTypes.number,
460
+ videoThumbWidth: PropTypes.number,
461
+ videoThumbHeight: PropTypes.number,
462
+ }),
285
463
  suggestions: PropTypes.arrayOf(
286
464
  PropTypes.shape({
287
465
  type: PropTypes.string,