@capillarytech/creatives-library 8.0.318 → 8.0.320

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 (139) hide show
  1. package/constants/unified.js +15 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/utils/templateVarUtils.js +172 -0
  7. package/utils/tests/templateVarUtils.test.js +160 -0
  8. package/v2Components/CapTagList/index.js +10 -0
  9. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  16. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  19. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  20. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  21. package/v2Components/CommonTestAndPreview/index.js +693 -155
  22. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  23. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  24. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  25. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  26. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  27. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  29. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  30. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  31. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  32. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  33. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  34. package/v2Components/FormBuilder/index.js +7 -1
  35. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  36. package/v2Components/SmsFallback/constants.js +73 -0
  37. package/v2Components/SmsFallback/index.js +956 -0
  38. package/v2Components/SmsFallback/index.scss +265 -0
  39. package/v2Components/SmsFallback/messages.js +78 -0
  40. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  41. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  42. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  43. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  44. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  45. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  46. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  47. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  48. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  49. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  50. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  51. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  52. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  53. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  54. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  55. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  56. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  57. package/v2Containers/CommunicationFlow/constants.js +200 -0
  58. package/v2Containers/CommunicationFlow/index.js +102 -0
  59. package/v2Containers/CommunicationFlow/messages.js +346 -0
  60. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  61. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  62. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  63. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  64. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  65. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  66. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  67. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  68. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  69. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  70. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  71. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  72. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  75. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  76. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  77. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  78. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  79. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  80. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  81. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  82. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  83. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  84. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  85. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  86. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  87. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  88. package/v2Containers/CreativesContainer/constants.js +12 -0
  89. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  90. package/v2Containers/CreativesContainer/index.js +289 -99
  91. package/v2Containers/CreativesContainer/index.scss +51 -1
  92. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  93. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  94. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  95. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  96. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  97. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  98. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  99. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  100. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  101. package/v2Containers/Rcs/constants.js +32 -1
  102. package/v2Containers/Rcs/index.js +950 -873
  103. package/v2Containers/Rcs/index.scss +85 -6
  104. package/v2Containers/Rcs/messages.js +10 -1
  105. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  106. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  107. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  108. package/v2Containers/Rcs/tests/index.test.js +41 -38
  109. package/v2Containers/Rcs/tests/mockData.js +38 -0
  110. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  111. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  112. package/v2Containers/Rcs/utils.js +358 -10
  113. package/v2Containers/Sms/Create/index.js +81 -36
  114. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  115. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  116. package/v2Containers/SmsTrai/Create/index.js +9 -4
  117. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  118. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  119. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  120. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  121. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  122. package/v2Containers/SmsWrapper/index.js +37 -8
  123. package/v2Containers/TagList/index.js +6 -0
  124. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  125. package/v2Containers/Templates/_templates.scss +61 -2
  126. package/v2Containers/Templates/actions.js +11 -0
  127. package/v2Containers/Templates/constants.js +2 -0
  128. package/v2Containers/Templates/index.js +90 -40
  129. package/v2Containers/Templates/sagas.js +57 -12
  130. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  131. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  132. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  133. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  134. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  135. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  136. package/v2Containers/TemplatesV2/index.js +86 -23
  137. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  138. package/v2Containers/Whatsapp/index.js +3 -20
  139. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,522 @@
1
+ /**
2
+ * ChannelSelectionStep Component
3
+ *
4
+ * Content template selection with channel dropdown
5
+ */
6
+
7
+ import React, { useState, useCallback, useMemo } from 'react';
8
+ import PropTypes from 'prop-types';
9
+ import { injectIntl } from 'react-intl';
10
+ import CapButton from '@capillarytech/cap-ui-library/CapButton';
11
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
12
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
13
+ import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
14
+ import CapMenu from '@capillarytech/cap-ui-library/CapMenu';
15
+ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
16
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
17
+ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
18
+ import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
19
+ import CreativesContainer from '../../../CreativesContainer';
20
+ import { DeliverySettingsSection } from '../DeliverySettingsStep';
21
+ import { CHANNELS } from '../../constants';
22
+ import { VIBER, MOBILE_PUSH, MPUSH, SMS, EMAIL } from '../../../CreativesContainer/constants';
23
+ // TemplatesV2 pane keys (for channelsToHide) - derived from CHANNELS + FTP
24
+ const CREATIVES_PANE_KEYS = [...CHANNELS?.map((channel) => channel?.paneKey), 'FTP'];
25
+ // Channels that TemplatesV2 shows only when enableNewChannels includes them (not in commonChannels)
26
+ const ENABLE_NEW_CHANNELS = CHANNELS.filter((channel) => channel.requiresEnableNewChannels).map((channel) => channel?.value);
27
+ import messages from '../../messages';
28
+ import './ChannelSelectionStep.scss';
29
+
30
+ // cap-coupons flows commented out
31
+ // import { CouponsWrapper, CouponsPointsStrategy } from '@capillarytech/cap-coupons';
32
+
33
+ const generateContentId = () => `content_${Date.now()}`;
34
+
35
+ const ChannelSelectionStep = ({
36
+ value,
37
+ channels, // eslint-disable-line
38
+ communicationStrategy, // eslint-disable-line
39
+ onChange,
40
+ channelsToHide = [],
41
+ channelsToDisable = [],
42
+ creativesMode = 'create',
43
+ selectedOfferDetails = [],
44
+ incentivesData,
45
+ deliverySettingsData,
46
+ error,
47
+ intl,
48
+ capData, // From Redux - contains user/org info needed by CouponsWrapper
49
+ }) => {
50
+ const contentItems = value?.contentItems || [];
51
+ const [showCreativesContainer, setShowCreativesContainer] = useState(false);
52
+ const [selectedChannel, setSelectedChannel] = useState(null);
53
+ const [editingContentId, setEditingContentId] = useState(null);
54
+ const [showChannelsMenu, setShowChannelsMenu] = useState(false);
55
+ // Keyed by contentId (or 'standalone' for the empty-state button) so each dropdown controls its own open state
56
+ const [showIncentivesMenuMap, setShowIncentivesMenuMap] = useState({});
57
+ const { formatMessage } = intl || {};
58
+
59
+ // Available channels (filter out hidden ones)
60
+ // Fallback to CHANNELS constant if channels prop is not provided
61
+ const channelsList = channels || CHANNELS;
62
+ const availableChannels = useMemo(() => channelsList?.filter((channel) => channel?.isActive), [channelsList]);
63
+ const availableIncentiveTypes = useMemo(() => incentivesData?.types?.filter((incentive) => incentive?.isActive), [incentivesData]);
64
+ // Derive channel config from CHANNELS for CreativesContainer
65
+ const selectedChannelConfig = useMemo(() => {
66
+ if (!selectedChannel) return null;
67
+ const selectedChannelLowerCase = selectedChannel?.toLowerCase();
68
+ return CHANNELS.find((channel) => channel?.value?.toLowerCase() === selectedChannelLowerCase || channel?.channelProp === selectedChannelLowerCase) || null;
69
+ }, [selectedChannel]);
70
+ /**
71
+ * Handle CreativesContainer close
72
+ */
73
+ const handleCloseCreatives = useCallback(() => {
74
+ setShowCreativesContainer(false);
75
+ setSelectedChannel(null);
76
+ setEditingContentId(null);
77
+ setShowChannelsMenu(false);
78
+ }, []);
79
+
80
+ /**
81
+ * Handle CreativesContainer data callback - persist content for create/edit
82
+ */
83
+ const handleCreativesData = useCallback((data) => {
84
+ if (!selectedChannel || !data) return;
85
+ const channelValue = CHANNELS.find((channel) => channel?.channelProp === selectedChannel)?.value || selectedChannel.toUpperCase();
86
+ const newItem = {
87
+ contentId: editingContentId || generateContentId(),
88
+ channel: channelValue,
89
+ templateData: data,
90
+ };
91
+ const updatedItems = editingContentId
92
+ ? contentItems.map((item) => (item.contentId === editingContentId ? { ...item, ...newItem, templateData: data } : item))
93
+ : [...contentItems, newItem];
94
+ onChange({ contentItems: updatedItems });
95
+ setShowCreativesContainer(false);
96
+ setSelectedChannel(null);
97
+ setEditingContentId(null);
98
+ }, [selectedChannel, editingContentId, contentItems, onChange]);
99
+
100
+ const handleEditContent = useCallback((contentId) => {
101
+ const item = contentItems.find((c) => c.contentId === contentId);
102
+ if (!item) return;
103
+ const channelConfig = CHANNELS.find((channel) => channel?.value === item?.channel || channel?.channelProp === item?.channel?.toLowerCase());
104
+ if (channelConfig) {
105
+ setSelectedChannel(channelConfig.channelProp);
106
+ setEditingContentId(contentId);
107
+ setShowCreativesContainer(true);
108
+ }
109
+ }, [contentItems]);
110
+
111
+ const handleDeleteContent = useCallback((contentId) => {
112
+ onChange({ contentItems: contentItems.filter((c) => c.contentId !== contentId) });
113
+ }, [contentItems, onChange]);
114
+
115
+ /** Get short preview text for content card */
116
+ const getContentPreview = (item) => {
117
+ const templateData = item?.templateData || {};
118
+ const channel = item?.channel?.toUpperCase();
119
+ if (templateData?.messageBody) return templateData.messageBody?.substring?.(0, 100) || templateData.messageBody;
120
+ if (templateData?.emailSubject) return templateData.emailSubject;
121
+ if (templateData?.smsBody) return templateData.smsBody?.substring?.(0, 100) || templateData.smsBody;
122
+ if (templateData?.message) return templateData.message?.substring?.(0, 100) || templateData.message;
123
+ // Mobile push: notificationTitle, notificationBody, or android/ios content
124
+ if (channel === MOBILE_PUSH || channel === MPUSH) {
125
+ const title = templateData?.notificationTitle || templateData?.androidContent?.notificationTitle || templateData?.iosContent?.notificationTitle;
126
+ const body = templateData?.notificationBody || templateData?.androidContent?.body || templateData?.iosContent?.body || templateData?.androidContent?.notificationBody || templateData?.iosContent?.notificationBody;
127
+ if (title) return title;
128
+ if (body) return body?.substring?.(0, 100) || body;
129
+ }
130
+ // Viber: messageBody is JSON string, extract content.text (campaigns pattern)
131
+ if (channel === VIBER) {
132
+ try {
133
+ const parsed = typeof templateData?.messageBody === 'string' ? JSON.parse(templateData?.messageBody || '{}') : templateData?.messageBody;
134
+ const text = parsed?.content?.text || '';
135
+ return text ? text?.substring?.(0, 100) : item?.channel || '';
136
+ } catch {
137
+ return templateData?.messageBody?.substring?.(0, 100) || item?.channel || '';
138
+ }
139
+ }
140
+ return item?.channel || '';
141
+ };
142
+
143
+ /** Get sender details display for card footer (e.g. SMS sender ID) */
144
+ const getSenderDetailsDisplay = (item) => {
145
+ const templateData = item?.templateData || {};
146
+ const channel = item?.channel?.toUpperCase();
147
+ if (channel === SMS) {
148
+ const senderId = templateData?.templateConfigs?.registeredSenderIds || templateData?.header;
149
+ return senderId ? `SMS sender ID ${senderId}` : null;
150
+ }
151
+ if (channel === EMAIL) return templateData?.emailFrom || null;
152
+ // Viber: sender from templateConfigs (campaigns/adiona pattern)
153
+ if (channel === VIBER) {
154
+ const senderId = templateData?.templateConfigs?.viberSenderId || templateData?.viberSenderId;
155
+ return senderId ? `Viber sender ID ${senderId}` : null;
156
+ }
157
+ return null;
158
+ };
159
+
160
+ /** Map channel to CapCustomCard type for styling (like adiona getEngagementCardType) */
161
+ const getCardType = (channel) => {
162
+ const upperCaseChannel = channel?.toUpperCase();
163
+ const mapping = { MOBILEPUSH: MPUSH, MPUSH: MPUSH };
164
+ return mapping[upperCaseChannel] || channel;
165
+ };
166
+
167
+ const handlePreviewContent = useCallback((item) => {
168
+ // Placeholder for preview - can be wired to Preview slidebox when available
169
+ // eslint-disable-next-line no-console
170
+ console.log('Preview content', item?.contentId);
171
+ }, []);
172
+
173
+ const handleChannelSelect = (channelValue) => {
174
+ // Check if channel is supported (not in channelsToHide or channelsToDisable)
175
+ if (channelsToHide.includes(channelValue) || channelsToDisable.includes(channelValue)) {
176
+ return;
177
+ }
178
+
179
+ // Find the channel object to verify it's active
180
+ const selectedChannelObj = availableChannels?.find((channel) => channel?.value === channelValue);
181
+ if (!selectedChannelObj || !selectedChannelObj?.isActive) {
182
+ return;
183
+ }
184
+
185
+ // Update parent component with selected channel
186
+ onChange({ channel: channelValue, channels: [] });
187
+
188
+ // Open CreativesContainer - derive channelProp from config
189
+ const lowerCaseChannelValue = channelValue?.toLowerCase();
190
+ const channelConfig = lowerCaseChannelValue && CHANNELS.find((channel) => channel?.value?.toLowerCase() === lowerCaseChannelValue || channel?.channelProp === lowerCaseChannelValue);
191
+ if (!channelConfig?.channelProp || channelConfig.channelProp === 'ftp') {
192
+ return;
193
+ }
194
+
195
+ setSelectedChannel(channelConfig.channelProp);
196
+ setShowCreativesContainer(true);
197
+ };
198
+
199
+ const handleChannelMenuClick = ({ key }) => {
200
+ // key should be the channel value (e.g., 'SMS', 'EMAIL', etc.)
201
+ handleChannelSelect(key);
202
+ setShowChannelsMenu(false);
203
+ };
204
+
205
+ const handleIncentiveSelect = (incentiveType) => {
206
+ // Update selectedOfferDetails with the selected incentive
207
+ const updatedOfferDetails = [...(selectedOfferDetails || []), { type: incentiveType }];
208
+ onChange({ selectedOfferDetails: updatedOfferDetails });
209
+ };
210
+
211
+ const handleIncentiveMenuClick = ({ key }, cardId) => {
212
+ setShowIncentivesMenuMap((prev) => ({ ...prev, [cardId]: false }));
213
+ // cap-coupons flows disabled - skip coupons/points
214
+ if (key === 'coupons' || key === 'points') return;
215
+ handleIncentiveSelect(key);
216
+ };
217
+
218
+ const handleChannelsVisibleChange = (visible) => {
219
+ setShowChannelsMenu(visible);
220
+ };
221
+
222
+ const handleIncentivesVisibleChange = (visible, cardId) => {
223
+ setShowIncentivesMenuMap((prev) => ({ ...prev, [cardId]: visible }));
224
+ };
225
+
226
+ /* cap-coupons flows disabled - handlers commented out
227
+ const handleClaimCoupon = useCallback((claimedCouponData) => {...}, [selectedOfferDetails, onChange]);
228
+ const handleEditCoupon = useCallback(() => {}, []);
229
+ const handleCreateCoupon = useCallback(() => {}, []);
230
+ const handleViewCoupon = useCallback(() => {}, []);
231
+ const handleEditCouponAction = useCallback(() => {}, []);
232
+ const handleTogglePreview = useCallback(() => {}, []);
233
+ const handleToggleAdvanceView = useCallback(() => {}, []);
234
+ const handleCancelCreateCoupon = useCallback(() => {}, []);
235
+ const handleClaimPoints = useCallback((claimedPointsData) => {...}, [selectedOfferDetails, onChange]);
236
+ const handleCloseCouponsSlidebox = useCallback(() => {}, []);
237
+ const handleClosePointsSlidebox = useCallback(() => {}, []);
238
+ */
239
+
240
+ const renderChannelDropdownOverlay = () => {
241
+ if (!availableChannels || availableChannels.length === 0) {
242
+ return null;
243
+ }
244
+
245
+ // Filter out FTP and ensure only active channels are shown
246
+ const filteredChannels = availableChannels.filter((channel) => {
247
+ const isFTP = channel.value === 'FTP' || channel.value === 'ftp';
248
+ const isHidden = channelsToHide.includes(channel.value);
249
+ const isDisabled = channelsToDisable.includes(channel.value);
250
+ return !isFTP && !isHidden && channel.isActive;
251
+ });
252
+
253
+ if (filteredChannels.length === 0) {
254
+ return null;
255
+ }
256
+
257
+ return (
258
+ <CapMenu onClick={handleChannelMenuClick} className="channel-dropdown-content">
259
+ {filteredChannels.map((channel) => {
260
+ const isDisabled = channelsToDisable.includes(channel.value);
261
+ const iconType = channel.iconType;
262
+ const displayLabel = channel.label || channel.value;
263
+
264
+ return (
265
+ <CapMenu.Item key={channel.value} disabled={isDisabled}>
266
+ <CapRow align="middle" type="flex" className="channel-row">
267
+ <CapIcon type={iconType} className="channel-icon" size="s" />
268
+ <CapHeading type="h5">{displayLabel}</CapHeading>
269
+ </CapRow>
270
+ </CapMenu.Item>
271
+ );
272
+ })}
273
+ </CapMenu>
274
+ );
275
+ };
276
+
277
+ const renderIncentiveDropdownOverlay = (onMenuClick) => {
278
+ if (availableIncentiveTypes?.length === 0) {
279
+ return null;
280
+ }
281
+ const filteredIncentiveTypes = availableIncentiveTypes?.filter(
282
+ (incentive) => incentive?.value !== 'coupons' && incentive?.value !== 'points',
283
+ );
284
+ if (filteredIncentiveTypes?.length === 0) return null;
285
+
286
+ return (
287
+ <CapMenu onClick={onMenuClick} className="incentive-dropdown-content">
288
+ {filteredIncentiveTypes.map((incentive) => {
289
+ const isDisabled = incentive?.disabled || false;
290
+ return (
291
+ <CapMenu.Item key={incentive.value} disabled={isDisabled}>
292
+ {incentive.label}
293
+ </CapMenu.Item>
294
+ );
295
+ })}
296
+ </CapMenu>
297
+ );
298
+ };
299
+
300
+ /** Configured state: CapCustomCard per content item - matches campaigns-ui structure */
301
+ const renderConfiguredCard = (item) => {
302
+ const channelConfig = CHANNELS.find((c) => c.value === item.channel || c.channelProp === item.channel?.toLowerCase());
303
+ const iconType = channelConfig?.iconType || 'sms';
304
+ const channelLabel = channelConfig?.label || item.channel;
305
+ const senderDetailsText = getSenderDetailsDisplay(item);
306
+ const hasIncentiveOptions = incentivesData && availableIncentiveTypes?.some((i) => i.value !== 'coupons' && i.value !== 'points');
307
+
308
+ const card = {
309
+ title: (
310
+ <CapRow align="middle" type="flex" className="card-title-row">
311
+ <CapIcon type={iconType} className="channel-icon" size="s" />
312
+ <CapHeader size="label1" titleClass="truncate-text" title={<span title={channelLabel}>{channelLabel}</span>} />
313
+ </CapRow>
314
+ ),
315
+ content: (
316
+ <CapRow className="content-card-body">
317
+ <CapLabel type="label2" className="content-preview-text">
318
+ {getContentPreview(item)}
319
+ </CapLabel>
320
+ {senderDetailsText && (
321
+ <CapRow type="flex" align="middle" justify="space-between" className="sender-details-row">
322
+ <CapLabel type="label2">{senderDetailsText}</CapLabel>
323
+ <CapIcon type="arrow-right" size="s" className="sender-details-arrow" />
324
+ </CapRow>
325
+ )}
326
+ </CapRow>
327
+ ),
328
+ cardType: getCardType(item.channel),
329
+ extra: (
330
+ <CapDropdown
331
+ overlay={
332
+ <CapMenu>
333
+ <CapMenu.Item
334
+ id="edit-menu-item"
335
+ className="ant-dropdown-menu-item"
336
+ onClick={() => handleEditContent(item.contentId)}
337
+ >
338
+ {formatMessage(messages.edit)}
339
+ </CapMenu.Item>
340
+ <CapMenu.Item
341
+ id="preview-menu-item"
342
+ className="ant-dropdown-menu-item"
343
+ onClick={() => handlePreviewContent(item)}
344
+ >
345
+ {formatMessage(messages.previewAndTest)}
346
+ </CapMenu.Item>
347
+ <CapMenu.Item
348
+ id="delete-menu-item"
349
+ onClick={() => handleDeleteContent(item.contentId)}
350
+ >
351
+ {formatMessage(messages.remove)}
352
+ </CapMenu.Item>
353
+ </CapMenu>
354
+ }
355
+ >
356
+ <CapIcon type="more" aria-label="Show more content options icon" />
357
+ </CapDropdown>
358
+ ),
359
+ };
360
+
361
+ // Add incentive in actions (campaigns-ui style: cap-card-action-container)
362
+ if (hasIncentiveOptions) {
363
+ card.actions = [
364
+ <CapRow className="cap-card-action-container add-incentive-action-row" key="add-incentive">
365
+ <CapDropdown
366
+ overlay={renderIncentiveDropdownOverlay((menuClickEvent) => handleIncentiveMenuClick(menuClickEvent, item.contentId))}
367
+ onVisibleChange={(visible) => handleIncentivesVisibleChange(visible, item.contentId)}
368
+ visible={!!showIncentivesMenuMap[item.contentId]}
369
+ >
370
+ <CapButton isAddBtn type="flat" className="add-incentives-button add-incentive-link">
371
+ {formatMessage(messages.addIncentive)}
372
+ </CapButton>
373
+ </CapDropdown>
374
+ <CapLabel type="label1" className="optional-incentive-label">
375
+ {formatMessage(messages.optional)}
376
+ </CapLabel>
377
+ </CapRow>,
378
+ ];
379
+ }
380
+
381
+ return card;
382
+ };
383
+
384
+ return (
385
+ <>
386
+ <CapRow className="channel-selection-step">
387
+ <CapHeading type="h3" className="margin-b-8">
388
+ {formatMessage(messages.contentTemplate)}
389
+ </CapHeading>
390
+
391
+ <CapRow
392
+ className={`content-template-container ${contentItems?.length > 0 ? 'content-template-container--has-content' : ''}`}
393
+ >
394
+ {contentItems?.length === 0 ? (
395
+ <CapRow className="content-template-section">
396
+ <CapHeading type="h5">
397
+ {formatMessage(messages.addMessageContentAndIncentive)}
398
+ </CapHeading>
399
+
400
+ <CapDropdown
401
+ overlay={renderChannelDropdownOverlay()}
402
+ onVisibleChange={handleChannelsVisibleChange}
403
+ visible={showChannelsMenu}
404
+ >
405
+ <CapButton
406
+ isAddBtn
407
+ type="primary"
408
+ className="add-content-template-button"
409
+ >
410
+ {formatMessage(messages.addContentTemplate)}
411
+ </CapButton>
412
+ </CapDropdown>
413
+ </CapRow>
414
+ ) : (
415
+ <CapCustomCard.CapCustomCardList
416
+ cardList={contentItems.map(renderConfiguredCard)}
417
+ className="content-items-list"
418
+ />
419
+ )}
420
+
421
+ {contentItems?.length === 0 &&
422
+ incentivesData &&
423
+ availableIncentiveTypes?.some((i) => i.value !== 'coupons' && i.value !== 'points') && (
424
+ <CapRow type="flex" justify="center" align="middle" className="incentive-section">
425
+ <CapDropdown
426
+ overlay={renderIncentiveDropdownOverlay((menuClickEvent) => handleIncentiveMenuClick(menuClickEvent, 'standalone'))}
427
+ onVisibleChange={(visible) => handleIncentivesVisibleChange(visible, 'standalone')}
428
+ visible={!!showIncentivesMenuMap.standalone}
429
+ >
430
+ <CapButton isAddBtn type="flat" className="add-incentive-button">
431
+ {formatMessage(messages.addIncentive)}
432
+ </CapButton>
433
+ </CapDropdown>
434
+ <CapHeading type="label4" className="optional-text">
435
+ {formatMessage(messages.optional)}
436
+ </CapHeading>
437
+ </CapRow>
438
+ )}
439
+
440
+ {contentItems?.length > 0 && (
441
+ <DeliverySettingsSection
442
+ contentItems={contentItems}
443
+ deliverySettingsData={deliverySettingsData}
444
+ deliverySetting={value?.deliverySetting}
445
+ onDeliverySettingChange={(deliverySetting) => onChange({ deliverySetting })}
446
+ intl={intl}
447
+ />
448
+ )}
449
+ </CapRow>
450
+
451
+ {error && (
452
+ <CapRow className="validation-error">
453
+ {error}
454
+ </CapRow>
455
+ )}
456
+ </CapRow>
457
+
458
+ {showCreativesContainer && selectedChannelConfig && (
459
+ <CreativesContainer
460
+ key={`creatives-${selectedChannel}-${editingContentId || 'create'}`}
461
+ channel={selectedChannelConfig.channelProp}
462
+ creativesMode={editingContentId ? 'edit' : creativesMode}
463
+ getCreativesData={handleCreativesData}
464
+ handleCloseCreatives={handleCloseCreatives}
465
+ isFullMode={false}
466
+ messageDetails={{ type: 'default' }}
467
+ templateData={editingContentId ? contentItems.find((c) => c.contentId === editingContentId)?.templateData : null}
468
+ selectedOfferDetails={selectedOfferDetails}
469
+ channelsToHide={[
470
+ ...(channelsToHide || []),
471
+ ...CREATIVES_PANE_KEYS.filter((key) => key !== selectedChannelConfig.paneKey),
472
+ ]}
473
+ channelsToDisable={channelsToDisable}
474
+ enableNewChannels={ENABLE_NEW_CHANNELS}
475
+ />
476
+ )}
477
+
478
+ {/* cap-coupons flows disabled - Coupons and Points slideboxes commented out */}
479
+ </>
480
+ );
481
+ };
482
+
483
+ ChannelSelectionStep.propTypes = {
484
+ value: PropTypes.object, // stepData (channel, contentItems, etc.)
485
+ channels: PropTypes.arrayOf(PropTypes.shape({
486
+ value: PropTypes.string.isRequired,
487
+ label: PropTypes.string,
488
+ isActive: PropTypes.bool,
489
+ iconType: PropTypes.string,
490
+ channelProp: PropTypes.string,
491
+ paneKey: PropTypes.string,
492
+ })),
493
+ communicationStrategy: PropTypes.string,
494
+ onChange: PropTypes.func.isRequired,
495
+ channelsToHide: PropTypes.array,
496
+ channelsToDisable: PropTypes.array,
497
+ creativesMode: PropTypes.oneOf(['create', 'edit', 'preview']),
498
+ selectedOfferDetails: PropTypes.array,
499
+ incentivesData: PropTypes.shape({
500
+ types: PropTypes.array,
501
+ required: PropTypes.bool,
502
+ }),
503
+ deliverySettingsData: PropTypes.object,
504
+ error: PropTypes.string,
505
+ intl: PropTypes.object.isRequired,
506
+ capData: PropTypes.object, // Cap data from Redux (user/org info)
507
+ };
508
+
509
+ ChannelSelectionStep.defaultProps = {
510
+ value: null,
511
+ channels: [],
512
+ communicationStrategy: null,
513
+ error: null,
514
+ channelsToHide: [],
515
+ channelsToDisable: [],
516
+ creativesMode: 'create',
517
+ selectedOfferDetails: [],
518
+ incentivesData: null,
519
+ capData: {},
520
+ };
521
+
522
+ export default injectIntl(ChannelSelectionStep);
@@ -0,0 +1,170 @@
1
+ @import '~@capillarytech/cap-ui-library/styles/_variables.scss';
2
+
3
+ .channel-selection-step {
4
+ .content-template-container {
5
+ border-radius: 0.25rem; // 4px
6
+ border: 1px solid $CAP_G07; // #DFE2E7
7
+ background: $CAP_WHITE; // #FFF
8
+ display: flex;
9
+ width: 17.0625rem; // 273px
10
+ height: 20rem; // 320px
11
+ padding-top: 1rem; // 16px
12
+ flex-direction: column;
13
+ justify-content: space-between;
14
+ align-items: center;
15
+
16
+ // Configured state: remove container border (CapCustomCard has its own), grow to fit
17
+ &--has-content {
18
+ border: none;
19
+ background: transparent;
20
+ max-width: 100%;
21
+ min-width: 0;
22
+ height: auto;
23
+ min-height: 0;
24
+ }
25
+ }
26
+
27
+ // Configured state only - these elements exist ONLY when content is configured
28
+ .content-items-list {
29
+ width: 100%;
30
+ flex: 1;
31
+
32
+ .cap-custom-card-list-row {
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: $CAP_SPACE_12;
36
+ }
37
+
38
+ .cap-custom-card-list-col {
39
+ width: 100%;
40
+ }
41
+
42
+ .ant-card .ant-card-actions {
43
+ & > li {
44
+ text-align: unset;
45
+ color: unset;
46
+ }
47
+ .add-incentives-button {
48
+ margin-left: -0.5rem;
49
+ }
50
+ }
51
+ }
52
+
53
+ .cap-card-action-container {
54
+ display: flex;
55
+ align-items: center;
56
+ padding: 0.625rem 0;
57
+
58
+ &.add-incentive-action-row {
59
+ justify-content: flex-start;
60
+ gap: $CAP_SPACE_04;
61
+
62
+ .optional-incentive-label {
63
+ padding-left: 0;
64
+ color: $CAP_G05;
65
+ }
66
+ }
67
+
68
+ .add-incentive-link {
69
+ color: $CAP_BLUE01; // Blue link per Figma (#2466eb)
70
+ padding: 0;
71
+ margin-left: -0.5rem;
72
+ }
73
+ }
74
+
75
+ .content-card-body {
76
+ flex-direction: column;
77
+ gap: $CAP_SPACE_12;
78
+ align-items: flex-start;
79
+ min-width: 0;
80
+ overflow: hidden;
81
+
82
+ .content-preview-text {
83
+ width: 100%;
84
+ min-width: 0;
85
+ overflow: hidden;
86
+ // display: -webkit-box;
87
+ // -webkit-line-clamp: 3;
88
+ // -webkit-box-orient: vertical;
89
+ word-break: break-word;
90
+ overflow-wrap: break-word;
91
+ word-wrap: break-word;
92
+ }
93
+
94
+ .sender-details-row {
95
+ width: 100%;
96
+ margin-top: $CAP_SPACE_08;
97
+ padding-top: $CAP_SPACE_08;
98
+ border-top: 1px solid $CAP_G07;
99
+
100
+ .sender-details-arrow {
101
+ margin-left: $CAP_SPACE_08;
102
+ }
103
+ }
104
+ }
105
+
106
+ .card-title-row {
107
+ gap: $CAP_SPACE_08;
108
+
109
+ .channel-icon {
110
+ color: $CAP_G04;
111
+ }
112
+ }
113
+
114
+ // Empty state styles - unchanged from original
115
+ .content-template-section {
116
+ display: flex;
117
+ flex-direction: column;
118
+ align-items: center;
119
+ justify-content: center;
120
+ height: 15.75rem;
121
+ width: 100%;
122
+ padding: 0 1rem;
123
+ border-bottom: 1px solid $CAP_G07;
124
+ }
125
+
126
+ .add-content-template-button {
127
+ margin-top: $CAP_SPACE_16;
128
+ padding: $CAP_SPACE_12;
129
+ padding-right: $CAP_SPACE_16;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ }
134
+
135
+ .incentive-section {
136
+ width: 100%;
137
+ padding: $CAP_SPACE_08 0;
138
+ }
139
+
140
+ .add-incentive-button {
141
+ /* Styles handled by CapButton with isAddBtn prop */
142
+ display: inline-flex;
143
+ }
144
+
145
+ .incentive-dropdown-content {
146
+ min-width: 12rem; // 192px
147
+ }
148
+ }
149
+
150
+ .channel-dropdown-content {
151
+ .ant-dropdown-menu-item {
152
+ .channel-row {
153
+ gap: $CAP_SPACE_08;
154
+ .cap-icon-v2.channel-icon {
155
+ color: $CAP_G04;
156
+ }
157
+ .cap-icon-v2-zalo {
158
+ /* Override ALL hardcoded fills inside CapIcon SVGs */
159
+ color: $CAP_G04;
160
+ svg path,
161
+ svg use,
162
+ svg rect,
163
+ svg circle,
164
+ svg g {
165
+ fill: currentColor !important;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }