@capillarytech/creatives-library 9.0.13 → 9.0.14

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 (38) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +10 -0
  3. package/services/tests/api.test.js +83 -0
  4. package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +5 -3
  5. package/v2Components/CommonTestAndPreview/index.js +7 -0
  6. package/v2Components/NavigationBar/index.js +27 -0
  7. package/v2Components/NavigationBar/messages.js +4 -0
  8. package/v2Components/NavigationBar/tests/index.test.js +19 -0
  9. package/v2Components/NewCallTask/index.js +6 -1
  10. package/v2Components/TemplatePreview/index.js +4 -2
  11. package/v2Containers/Cap/index.js +3 -1
  12. package/v2Containers/CommunicationFlow/CommunicationFlow.js +130 -20
  13. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +154 -0
  14. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +240 -0
  15. package/v2Containers/CommunicationFlow/DemoPage.js +47 -0
  16. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +369 -2
  17. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +619 -0
  18. package/v2Containers/CommunicationFlow/Tests/DemoPage.test.js +77 -0
  19. package/v2Containers/CommunicationFlow/Tests/getContentBody.test.js +933 -0
  20. package/v2Containers/CommunicationFlow/constants.js +45 -10
  21. package/v2Containers/CommunicationFlow/index.js +5 -2
  22. package/v2Containers/CommunicationFlow/messages.js +20 -0
  23. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +94 -31
  24. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +14 -11
  25. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +1144 -32
  26. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/extractContentForPreview.js +183 -0
  27. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +3 -0
  28. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +39 -0
  29. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +6 -2
  30. package/v2Containers/CommunicationFlow/utils/getContentBody.js +369 -0
  31. package/v2Containers/CommunicationFlow/utils/getContentBody.scss +19 -0
  32. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +1 -1
  33. package/v2Containers/CreativesContainer/constants.js +6 -0
  34. package/v2Containers/CreativesContainer/index.js +68 -1
  35. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +2 -2
  36. package/v2Containers/Templates/index.js +2 -2
  37. package/v2Containers/TemplatesV2/index.js +9 -1
  38. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +41 -34
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.13",
4
+ "version": "9.0.14",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -693,6 +693,16 @@ export const createCentralCommsMetaId = (payload, metaType = TRANSACTION) => {
693
693
  return request(url, getAPICallObject('POST', payload, false, false, false, true));
694
694
  };
695
695
 
696
+ export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
697
+ const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
698
+ return request(url, getAPICallObject('GET', null, false, false, false, true));
699
+ };
700
+
701
+ export const bulkClaimAndApprove = (payload, metaType = TRANSACTION) => {
702
+ const url = `${API_ENDPOINT}/common/central-comms/bulk-claim-approve/${metaType}`;
703
+ return request(url, getAPICallObject('POST', payload, false, false, false, true));
704
+ };
705
+
696
706
  export const updateMetaConfig = (payload, metaType = TRANSACTION, metaId) => {
697
707
  const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}/${metaId}`;
698
708
  return request(url, getAPICallObject('PATCH', payload, false, false, false, true));
@@ -1092,6 +1092,8 @@ import {
1092
1092
  unarchiveTemplate,
1093
1093
  bulkArchiveTemplates,
1094
1094
  bulkUnarchiveTemplates,
1095
+ getCentralCommsMetaIds,
1096
+ bulkClaimAndApprove,
1095
1097
  } from '../api';
1096
1098
 
1097
1099
  describe('archiveTemplate', () => {
@@ -1145,3 +1147,84 @@ describe('bulkUnarchiveTemplates', () => {
1145
1147
  expect(lastCall[1].method).toBe('PUT');
1146
1148
  });
1147
1149
  });
1150
+
1151
+ describe('getCentralCommsMetaIds', () => {
1152
+ beforeEach(() => {
1153
+ global.fetch = jest.fn().mockReturnValue(Promise.resolve({
1154
+ status: 200,
1155
+ json: () => Promise.resolve({ status: 200, response: [] }),
1156
+ }));
1157
+ });
1158
+
1159
+ afterEach(() => {
1160
+ jest.restoreAllMocks();
1161
+ });
1162
+
1163
+ it('should return a Promise', () => {
1164
+ const result = getCentralCommsMetaIds('1,2,3');
1165
+ expect(result).toBeInstanceOf(Promise);
1166
+ });
1167
+
1168
+ it('should call GET on central-comms/meta-id endpoint with default TRANSACTION type', () => {
1169
+ getCentralCommsMetaIds('1,2,3');
1170
+ expect(global.fetch).toHaveBeenCalled();
1171
+ const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
1172
+ expect(lastCall[0]).toContain('central-comms/meta-id/TRANSACTION');
1173
+ expect(lastCall[0]).toContain('metaIds=1,2,3');
1174
+ expect(lastCall[1].method).toBe('GET');
1175
+ });
1176
+
1177
+ it('should use provided metaType in URL', () => {
1178
+ getCentralCommsMetaIds('4,5', 'EVENT');
1179
+ expect(global.fetch).toHaveBeenCalled();
1180
+ const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
1181
+ expect(lastCall[0]).toContain('central-comms/meta-id/EVENT');
1182
+ });
1183
+
1184
+ it('should handle fetch failure', async () => {
1185
+ global.fetch.mockRejectedValue({ error: 'Network error' });
1186
+ const result = await getCentralCommsMetaIds('1,2,3');
1187
+ expect(result).toEqual({ error: 'Network error' });
1188
+ });
1189
+ });
1190
+
1191
+ describe('bulkClaimAndApprove', () => {
1192
+ beforeEach(() => {
1193
+ global.fetch = jest.fn().mockReturnValue(Promise.resolve({
1194
+ status: 200,
1195
+ json: () => Promise.resolve({ status: 200, response: { success: true } }),
1196
+ }));
1197
+ });
1198
+
1199
+ afterEach(() => {
1200
+ jest.restoreAllMocks();
1201
+ });
1202
+
1203
+ it('should return a Promise', () => {
1204
+ const result = bulkClaimAndApprove({ ids: ['1', '2'] });
1205
+ expect(result).toBeInstanceOf(Promise);
1206
+ });
1207
+
1208
+ it('should call POST on central-comms/bulk-claim-approve endpoint with default TRANSACTION type', () => {
1209
+ const payload = { ids: ['1', '2'] };
1210
+ bulkClaimAndApprove(payload);
1211
+ expect(global.fetch).toHaveBeenCalled();
1212
+ const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
1213
+ expect(lastCall[0]).toContain('central-comms/bulk-claim-approve/TRANSACTION');
1214
+ expect(lastCall[1].method).toBe('POST');
1215
+ expect(lastCall[1].body).toBe(JSON.stringify(payload));
1216
+ });
1217
+
1218
+ it('should use provided metaType in URL', () => {
1219
+ bulkClaimAndApprove({ ids: ['3'] }, 'EVENT');
1220
+ expect(global.fetch).toHaveBeenCalled();
1221
+ const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
1222
+ expect(lastCall[0]).toContain('central-comms/bulk-claim-approve/EVENT');
1223
+ });
1224
+
1225
+ it('should handle fetch failure', async () => {
1226
+ global.fetch.mockRejectedValue({ error: 'Network error' });
1227
+ const result = await bulkClaimAndApprove({ ids: ['1'] });
1228
+ expect(result).toEqual({ error: 'Network error' });
1229
+ });
1230
+ });
@@ -17,7 +17,7 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
17
17
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
18
18
  import { ANDROID, IOS } from '../constants';
19
19
  import messages from '../messages';
20
- import { getWhatsappQuickReply, getWhatsappCarouselButtonView } from '../../../v2Containers/Whatsapp/utils';
20
+ import { getWhatsappQuickReply, getWhatsappCarouselButtonView, getWhatsappDocPreview } from '../../../v2Containers/Whatsapp/utils';
21
21
  import { QUICK_REPLY, PHONE_NUMBER, WHATSAPP_CATEGORIES, TEMPLATE_VARIABLE_REGEX } from '../../../v2Containers/Whatsapp/constants';
22
22
  import videoPlay from '../../../assets/videoPlay.svg';
23
23
  import whatsappImageEmptyPreview from '../../TemplatePreview/assets/images/empty_image_preview.svg';
@@ -258,8 +258,10 @@ const WhatsAppPreviewContent = ({
258
258
 
259
259
  {/* Document Preview */}
260
260
  {content?.docPreview && (
261
- <CapRow useLegacy className="whatsapp-image">
262
- {content.docPreview}
261
+ <CapRow className="whatsapp-image">
262
+ {React.isValidElement(content.docPreview)
263
+ ? content.docPreview
264
+ : getWhatsappDocPreview(content.docPreview)}
263
265
  </CapRow>
264
266
  )}
265
267
 
@@ -394,6 +394,13 @@ const CommonTestAndPreview = (props) => {
394
394
  return '';
395
395
  }
396
396
 
397
+ // WHATSAPP channel - return template message string (content is a complex object with JSX nodes,
398
+ // JSON.stringify would crash; the preview uses the content prop directly via WhatsAppPreviewContent)
399
+ if (channel === CHANNELS.WHATSAPP) {
400
+ if (typeof content === 'string') return content;
401
+ return content?.templateMsg || '';
402
+ }
403
+
397
404
  // For SMS and other string-based channels, return content as-is if it's already a string
398
405
  // Don't stringify strings as it adds unnecessary quotes
399
406
  if (channel === CHANNELS.SMS && typeof content === 'string') {
@@ -67,11 +67,23 @@ export class NavigationBar extends React.Component {
67
67
  return {
68
68
  selectedProduct: formatMessage(messages.loyaltyProgram),
69
69
  helpUrl: LOYALTY_HELP_URL,
70
+ settingsIcon: {
71
+ iconType: 'settings',
72
+ key: 'settings',
73
+ placement: 'bottomRight',
74
+ className: 'navigation-setting-icon',
75
+ toolTip: formatMessage(messages.loyaltyCreativeSettings),
76
+ },
70
77
  };
71
78
  default:
72
79
  return {
73
80
  selectedProduct: formatMessage(messages.selectedProductDefault),
74
81
  helpUrl: HELP_URL,
82
+ settingsIcon: {
83
+ iconType: 'settings',
84
+ key: 'settings',
85
+ onClickHandler: this.onSettingsIconClick,
86
+ },
75
87
  };
76
88
  }
77
89
  }
@@ -83,6 +95,16 @@ export class NavigationBar extends React.Component {
83
95
  }
84
96
  };
85
97
 
98
+ onSettingsIconClick = () => {
99
+ const { settingsUrl } = this.props;
100
+ if (settingsUrl) {
101
+ const stateObj = {
102
+ page: "settings",
103
+ };
104
+ window.history.pushState(stateObj, 'Settings', window.location.replace(settingsUrl));
105
+ }
106
+ };
107
+
86
108
  onHelpIconClick = () => {
87
109
  const { helpUrl } = this.state;
88
110
  if (helpUrl) {
@@ -91,6 +113,7 @@ export class NavigationBar extends React.Component {
91
113
  };
92
114
 
93
115
  getTopbarIcons = (showDocumentationBot = false) => {
116
+ const { settingsIcon } = this.state;
94
117
  const ICONS = [
95
118
  {
96
119
  iconType: 'help',
@@ -98,6 +121,9 @@ export class NavigationBar extends React.Component {
98
121
  onClickHandler: this.onHelpIconClick,
99
122
  },
100
123
  ];
124
+ if (settingsIcon) {
125
+ ICONS.push(settingsIcon);
126
+ }
101
127
  return showDocumentationBot ? ICONS.slice(1) : ICONS; // If showDocumentationBot is true, help icon will be replaced by Aira icon on UI
102
128
  };
103
129
 
@@ -196,6 +222,7 @@ NavigationBar.propTypes = {
196
222
  topbarMenuData: PropTypes.array,
197
223
  logout: PropTypes.func,
198
224
  changeOrg: PropTypes.func,
225
+ settingsUrl: PropTypes.string,
199
226
  children: PropTypes.node,
200
227
  orgSettingsUrl: PropTypes.string,
201
228
  intl: intlShape.isRequired,
@@ -65,5 +65,9 @@ export default defineMessages({
65
65
  "selectOrganization": {
66
66
  id: `${scope}.selectOrganization`,
67
67
  defaultMessage: 'Select organization',
68
+ },
69
+ "loyaltyCreativeSettings": {
70
+ id: `${scope}.loyaltyCreativeSettings`,
71
+ defaultMessage: 'There are no settings for creatives within Loyalty+. Channel-related settings are available within organisation settings',
68
72
  }
69
73
  });
@@ -71,4 +71,23 @@ describe('NavigationBar', () => {
71
71
  const loyaltyProduct = screen.getByText('Loyalty+');
72
72
  expect(loyaltyProduct).toBeInTheDocument();
73
73
  });
74
+
75
+ // Regression: AntD v3 -> v6 migration (CAP-183930) dropped the settings (gear)
76
+ // icon from the top nav. These tests ensure the gear keeps rendering.
77
+ it('Should render the settings (gear) icon in the top nav bar', () => {
78
+ const updatedProps = cloneDeep(props);
79
+ updatedProps.location.pathname = '/creatives/ui/v2';
80
+ updatedProps.settingsUrl = '/campaigns/ui/creatives/settings/message';
81
+ renderComponent(updatedProps);
82
+ const settingsIcon = document.querySelector('.cap-icon-v2-settings');
83
+ expect(settingsIcon).toBeInTheDocument();
84
+ });
85
+
86
+ it('Should render the settings (gear) icon for the loyalty product', () => {
87
+ const updatedProps = cloneDeep(props);
88
+ updatedProps.location.pathname = '/creatives/ui/v2/loyalty';
89
+ renderComponent(updatedProps);
90
+ const settingsIcon = document.querySelector('.cap-icon-v2-settings.navigation-setting-icon');
91
+ expect(settingsIcon).toBeInTheDocument();
92
+ });
74
93
  });
@@ -16,6 +16,7 @@ import CallTaskPreview from '../CallTaskPreview';
16
16
  import messages from './messages';
17
17
  import './_newCallTask.scss';
18
18
  import { hasStore2DoorFeature } from '../../utils/common';
19
+ import { EDIT } from '../../constants/unified';
19
20
 
20
21
  const CALL = 'CALL';
21
22
  const STOREMAX_WHATSAPP = 'STOREMAX_WHATSAPP';
@@ -27,7 +28,11 @@ class NewCallTask extends React.Component { // eslint-disable-line react/prefer-
27
28
 
28
29
  getInitialState = () => {
29
30
  const { templateData, messageDetails, mode } = this.props;
30
- const actionType = mode === 'edit' ? messageDetails.messageContent.message_content_id_1.callTaskActionType : CALL;
31
+ const actionType = mode === EDIT
32
+ ? (messageDetails?.messageContent?.message_content_id_1?.callTaskActionType
33
+ || templateData?.callTaskActionType
34
+ || CALL)
35
+ : CALL;
31
36
  return {
32
37
  subject: templateData && templateData.subject ? templateData.subject : '',
33
38
  messageBody: templateData && templateData.messageBody ? templateData.messageBody : '',
@@ -47,7 +47,7 @@ import { handlePreviewInNewTab } from '../../utils/common';
47
47
  import { TEMPLATE, IMAGE_CAROUSEL, IMAGE, STICKER, TEXT, IMAGE_MAP, VIDEO } from '../../v2Containers/Line/Container/constants';
48
48
  import CapFacebookPreview from '../../v2Containers/CapFacebookPreview';
49
49
  import WhatsappStatusContainer from '../WhatsappStatusContainer';
50
- import { getWhatsappQuickReply, getWhatsappCarouselButtonView } from '../../v2Containers/Whatsapp/utils';
50
+ import { getWhatsappQuickReply, getWhatsappCarouselButtonView, getWhatsappDocPreview } from '../../v2Containers/Whatsapp/utils';
51
51
  import { QUICK_REPLY, WHATSAPP_CATEGORIES, PHONE_NUMBER, TEMPLATE_VARIABLE_REGEX } from '../../v2Containers/Whatsapp/constants';
52
52
  import { RCS_BUTTON_TYPES, LEFT, HORIZONTAL, VERTICAL, RIGHT} from '../../v2Containers/Rcs/constants';
53
53
  import { ANDROID, INAPP_MESSAGE_LAYOUT_TYPES } from '../../v2Containers/InApp/constants';
@@ -1234,7 +1234,9 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
1234
1234
  )}
1235
1235
  {content?.docPreview && (
1236
1236
  <div className="whatsapp-image">
1237
- {content?.docPreview}
1237
+ {React.isValidElement(content.docPreview)
1238
+ ? content.docPreview
1239
+ : getWhatsappDocPreview(content.docPreview)}
1238
1240
  </div>
1239
1241
  )}
1240
1242
  {content?.templateHeaderPreview || ""}
@@ -18,7 +18,7 @@ import * as locationActions from '../LanguageProvider/actions';
18
18
  import * as appActions from '../App/actions';
19
19
  import config from '../../config/app';
20
20
  import NavigationBar from '../../v2Components/NavigationBar';
21
- import { publicPath } from '../../config/path';
21
+ import { engagePlusPublicPath, publicPath } from '../../config/path';
22
22
  import { GTM_TRACKING_ID, CREATIVES_UI_VIEW, FAILURE } from '../App/constants';
23
23
  import { makeSelectLocale } from '../../v2Containers/LanguageProvider/selectors';
24
24
  import CapSupportVideosWrapper from '@capillarytech/cap-ui-library/CapSupportVideosWrapper';
@@ -30,6 +30,7 @@ import {
30
30
  REQUEST,
31
31
  DEFAULT,
32
32
  ENABLE_PRODUCT_SUPPORT_VIDEOS,
33
+ CAMPAIGN_SETTINGS_URL,
33
34
  } from './constants';
34
35
  import './_cap.scss';
35
36
  import { Switch } from 'react-router';
@@ -528,6 +529,7 @@ export class Cap extends React.Component { // eslint-disable-line react/prefer-s
528
529
  changeOrg={this.changeOrg}
529
530
  isCreativesAccessible={isCreativesAccessible}
530
531
  logout={this.logout}
532
+ settingsUrl={`${engagePlusPublicPath}${CAMPAIGN_SETTINGS_URL}`}
531
533
  orgSettingsUrl={ORG_SETTINGS_URL}
532
534
  loggedIn={isLoggedIn}
533
535
  topbarMenuData={topbarMenuDataOptions}
@@ -20,6 +20,7 @@ import CapButton from '@capillarytech/cap-ui-library/CapButton';
20
20
  // import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
21
21
  // import injectReducer from '../../utils/injectReducer';
22
22
  import { makeSelectAuthenticated } from '../Cap/selectors';
23
+ import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../services/api';
23
24
  import DynamicControlsStep from './steps/DynamicControlsStep';
24
25
  import MessageTypeStep from './steps/MessageTypeStep';
25
26
  import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
@@ -28,6 +29,15 @@ import {
28
29
  STEPS,
29
30
  CHANNEL_PRIORITY,
30
31
  AB_TEST,
32
+ DEFAULT_COMMUNICATION_STRATEGY_OPTIONS,
33
+ CHANNELS_WITHOUT_DELIVERY,
34
+ MESSAGE_TYPES_OPTIONS,
35
+ CHANNELS,
36
+ INCENTIVE_TYPES,
37
+ DYNAMIC_CONTROLS_CONFIG,
38
+ CHANNEL_CONTENT_KEY_MAP,
39
+ CHANNEL_DELIVERY_KEY_MAP,
40
+ CAMPAIGNS,
31
41
  } from './constants';
32
42
  import { getEnabledSteps } from './utils/getEnabledSteps';
33
43
  import messages from './messages';
@@ -43,6 +53,17 @@ import './CommunicationFlow.scss';
43
53
  // saga: CouponsCapContainer.couponsCapSaga,
44
54
  // });
45
55
 
56
+ const getDeliveryChannels = (contentItems) => {
57
+ const channels = contentItems.map((item) => (item.channel || '').toUpperCase()).filter(Boolean);
58
+ const unique = [...new Set(channels)];
59
+ return unique.filter((channel) => !CHANNELS_WITHOUT_DELIVERY.includes(channel));
60
+ };
61
+
62
+ const hasDeliverySettingForChannel = (channelSetting, channel) => {
63
+ const settings = channelSetting[channel];
64
+ return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
65
+ };
66
+
46
67
  const CommunicationFlow = ({
47
68
  config,
48
69
  initialData,
@@ -50,13 +71,14 @@ const CommunicationFlow = ({
50
71
  onCancel, // eslint-disable-line
51
72
  onChange,
52
73
  intl,
53
- capData, // From Redux - contains user/org info needed by CouponsWrapper
74
+ cap, // From parent consumer (e.g. campaigns passes campaignCap) takes priority over Redux capData
75
+ capData, // From Redux - fallback when cap is not provided by consumer
54
76
  }) => {
55
77
  const { formatMessage } = intl || {};
56
78
  const { messageTypeData = {}, communicationStrategyData = {}, contentTemplateData = {} } = config?.features || {};
57
79
  // Initialize step data from initialData or defaults
58
80
  const [stepData, setStepData] = useState(() => {
59
- const defaultMessageType = messageTypeData.defaultOption?.value || null;
81
+ const defaultMessageType = messageTypeData.defaultOption?.value || MESSAGE_TYPES_OPTIONS?.[1]?.value || null;
60
82
  return {
61
83
  messageType: initialData?.messageType || defaultMessageType,
62
84
  communicationStrategy: initialData?.communicationStrategy || null,
@@ -73,6 +95,27 @@ const CommunicationFlow = ({
73
95
  // Memoize enabled steps
74
96
  const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
75
97
 
98
+ const isSaveDisabled = useMemo(() => {
99
+ const strategyRequired = enabledSteps.includes(STEPS.COMMUNICATION_STRATEGY);
100
+ const templateRequired = enabledSteps.includes(STEPS.CHANNEL_SELECTION);
101
+ const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(stepData.communicationStrategy);
102
+
103
+ if (strategyRequired && !stepData.communicationStrategy) return true;
104
+ if (templateRequired && !isMultiChannel && (!stepData.contentItems || stepData.contentItems.length === 0)) return true;
105
+
106
+ const deliverySettingsEnabled = !!config?.features?.deliverySettingsData;
107
+ if (deliverySettingsEnabled && templateRequired) {
108
+ const deliveryChannels = getDeliveryChannels(stepData.contentItems || []);
109
+ const channelSetting = stepData.deliverySetting?.channelSetting || {};
110
+ const anyChannelMissingSenderDetails = deliveryChannels.some(
111
+ (channel) => !hasDeliverySettingForChannel(channelSetting, channel),
112
+ );
113
+ if (anyChannelMissingSenderDetails) return true;
114
+ }
115
+
116
+ return false;
117
+ }, [enabledSteps, stepData.communicationStrategy, stepData.contentItems, stepData.deliverySetting, config?.features?.deliverySettingsData]);
118
+
76
119
  /**
77
120
  * Get aggregated data from all steps
78
121
  */
@@ -106,6 +149,77 @@ const CommunicationFlow = ({
106
149
  }));
107
150
  }, []);
108
151
 
152
+ const handleSave = useCallback(async () => {
153
+ const aggregatedData = getAggregatedData();
154
+ const shouldUseCCS = config?.useCCS !== false;
155
+
156
+ if (shouldUseCCS) {
157
+ const ouId = config?.context?.ouId || -1;
158
+ const module = config?.context?.module
159
+ || (config?.consumer ? config.consumer.toUpperCase() : CAMPAIGNS);
160
+
161
+ const channelContentKeyMap = CHANNEL_CONTENT_KEY_MAP;
162
+ const channelDeliveryKeyMap = CHANNEL_DELIVERY_KEY_MAP;
163
+
164
+ const contentItems = aggregatedData.contentItems || [];
165
+ const { dynamicControls = {} } = aggregatedData;
166
+
167
+ const additionalSettings = {
168
+ useTinyUrl: dynamicControls.useTinyUrl ?? false,
169
+ encryptUrl: dynamicControls.sendToControlCustomers ?? false,
170
+ linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
171
+ userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
172
+ };
173
+
174
+ if (contentItems.length > 0) {
175
+ try {
176
+ const responses = await Promise.all(
177
+ contentItems.map((item) => {
178
+ const channel = (item.channel || '').toUpperCase();
179
+ const contentKey = channelContentKeyMap[channel];
180
+ const deliveryKey = channelDeliveryKeyMap[channel];
181
+ const payload = {
182
+ centralCommsPayload: {
183
+ ouId,
184
+ channel,
185
+ module,
186
+ executionParams: {},
187
+ clientName: 'EMF',
188
+ ...(contentKey && {
189
+ [contentKey]: { channel, ...item.templateData },
190
+ }),
191
+ ...(deliveryKey && {
192
+ [deliveryKey]: {
193
+ additionalSettings,
194
+ channelSettings: {
195
+ channel,
196
+ ...(aggregatedData.deliverySetting?.channelSetting?.[channel] || {}),
197
+ },
198
+ },
199
+ }),
200
+ },
201
+ };
202
+ return createCentralCommsMetaId(payload);
203
+ }),
204
+ );
205
+
206
+ const metaIds = responses
207
+ .map((res) => res?.response?.data?.id)
208
+ .filter(Boolean)
209
+ .join(',');
210
+
211
+ if (metaIds) {
212
+ const getResponse = await getCentralCommsMetaIds(metaIds);
213
+ }
214
+ } catch (error) {
215
+ console.error('[CommunicationFlow] CCS createCentralCommsMetaId error:', error);
216
+ }
217
+ }
218
+ }
219
+
220
+ onSave(aggregatedData);
221
+ }, [getAggregatedData, config, onSave]);
222
+
109
223
  // Call onChange callback when stepData changes
110
224
  useEffect(() => {
111
225
  if (onChange) {
@@ -131,8 +245,8 @@ const CommunicationFlow = ({
131
245
  <MessageTypeStep
132
246
  {...commonProps}
133
247
  value={stepData.messageType}
134
- options={messageTypeData.options}
135
- defaultOption={messageTypeData.defaultOption}
248
+ options={messageTypeData.options || MESSAGE_TYPES_OPTIONS}
249
+ defaultOption={messageTypeData.defaultOption || MESSAGE_TYPES_OPTIONS?.[1]}
136
250
  onChange={(messageType) => handleStepChange(step, { messageType })}
137
251
  />
138
252
  <CapDivider />
@@ -145,7 +259,7 @@ const CommunicationFlow = ({
145
259
  {...commonProps}
146
260
  value={stepData.communicationStrategy}
147
261
  // messageType={stepData.messageType}
148
- options={communicationStrategyData.options}
262
+ options={communicationStrategyData.options || DEFAULT_COMMUNICATION_STRATEGY_OPTIONS}
149
263
  disabled={communicationStrategyData.disabled}
150
264
  onChange={(communicationStrategy) => handleStepChange(step, { communicationStrategy })}
151
265
  />
@@ -162,30 +276,23 @@ const CommunicationFlow = ({
162
276
  <ChannelSelectionStep
163
277
  {...commonProps}
164
278
  value={stepData}
165
- channels={contentTemplateData.channels}
279
+ channels={contentTemplateData.channels || CHANNELS}
166
280
  onChange={(data) => handleStepChange(step, data)}
167
281
  channelsToHide={contentTemplateData.channelsToHide}
168
282
  channelsToDisable={contentTemplateData.channelsToDisable}
169
283
  creativesMode={config.mode || 'create'}
170
284
  selectedOfferDetails={stepData.selectedOfferDetails}
171
- incentivesData={config.features?.incentivesData}
285
+ incentivesData={{
286
+ ...config.features?.incentivesData,
287
+ types: config.features?.incentivesData?.types || INCENTIVE_TYPES,
288
+ }}
172
289
  deliverySettingsData={config.features?.deliverySettingsData}
173
290
  config={config}
174
- capData={capData}
291
+ capData={cap || capData}
175
292
  />
176
293
  <CapDivider />
177
294
  </CapRow>
178
295
  );
179
- // This will be added back in when coupons/points are integrated so keeping it commented out for now
180
- // case STEPS.INCENTIVES:
181
- // return (
182
- // <IncentivesStep
183
- // key={step}
184
- // {...commonProps}
185
- // value={stepData.selectedOfferDetails}
186
- // onChange={(selectedOfferDetails) => handleStepChange(step, { selectedOfferDetails })}
187
- // />
188
- // );
189
296
  case STEPS.DYNAMIC_CONTROLS:
190
297
  // Only show DynamicControlsStep if communication strategy is selected
191
298
  if (!stepData.communicationStrategy) {
@@ -196,7 +303,7 @@ const CommunicationFlow = ({
196
303
  key={step}
197
304
  {...commonProps}
198
305
  value={{ dynamicControls: stepData.dynamicControls }}
199
- controls={config.features?.dynamicControlsData?.controls || []}
306
+ controls={config.features?.dynamicControlsData?.controls || DYNAMIC_CONTROLS_CONFIG}
200
307
  onChange={(data) => handleStepChange(step, { dynamicControls: data.dynamicControls })}
201
308
  />
202
309
  );
@@ -210,7 +317,7 @@ const CommunicationFlow = ({
210
317
  {renderSteps()}
211
318
  {onSave && (
212
319
  <CapRow useLegacy className="communication-flow-container__footer">
213
- <CapButton type="primary" onClick={() => onSave(getAggregatedData())}>
320
+ <CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
214
321
  {formatMessage(messages.save)}
215
322
  </CapButton>
216
323
  </CapRow>
@@ -262,18 +369,21 @@ CommunicationFlow.propTypes = {
262
369
  incentivesTypes: PropTypes.arrayOf(PropTypes.oneOf(['coupons', 'points', 'promotions', 'giftVouchers', 'badges'])),
263
370
  }),
264
371
  context: PropTypes.object,
372
+ useCCS: PropTypes.bool,
265
373
  }).isRequired,
266
374
  initialData: PropTypes.object,
267
375
  onSave: PropTypes.func.isRequired,
268
376
  onCancel: PropTypes.func.isRequired,
269
377
  onChange: PropTypes.func,
270
378
  intl: PropTypes.object.isRequired,
379
+ cap: PropTypes.object,
271
380
  capData: PropTypes.object,
272
381
  };
273
382
 
274
383
  CommunicationFlow.defaultProps = {
275
384
  initialData: null,
276
385
  onChange: null,
386
+ cap: null,
277
387
  capData: {},
278
388
  };
279
389