@capillarytech/creatives-library 9.0.54 → 9.0.55-alpha.0

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.
@@ -38,6 +38,95 @@ export const CHANNEL_PRIORITY = 'CHANNEL_PRIORITY';
38
38
  export const AB_TEST = 'AB_TEST';
39
39
  export const SINGLE_TEMPLATE = 'SINGLE_TEMPLATE';
40
40
 
41
+ // CCS CommDefinition strategyType — distinct from SINGLE_TEMPLATE above, which
42
+ // is this UI's own communicationStrategy value. Only SINGLE is supported this
43
+ // phase; CCS create is skipped entirely for CHANNEL_PRIORITY/AB_TEST.
44
+ export const CCS_STRATEGY_TYPE_SINGLE = 'SINGLE';
45
+
46
+ // Mirrors CCS's SingleChannelStrategy field names exactly (Cap Notify API
47
+ // Design doc sample payloads, verified against real create responses). Kept
48
+ // separate from CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP below, which
49
+ // are the legacy messageMeta payload's keys.
50
+ export const CCS_CHANNEL_CONTENT_KEY_MAP = {
51
+ SMS: 'smsMessageContent',
52
+ EMAIL: 'emailMessageContent',
53
+ WHATSAPP: 'whatsappMessageContent',
54
+ // CCS's channel enum value for mobile push is 'PUSH' (verified against real
55
+ // create responses), even though the content/delivery keys stay 'mpush*'.
56
+ PUSH: 'mpushMessageContent',
57
+ INAPP: 'inAppMessageContent',
58
+ ANDROID: 'androidMessageContent',
59
+ IOS: 'iosMessageContent',
60
+ ZALO: 'zaloMessageContent',
61
+ VIBER: 'viberMessageContent',
62
+ LINE: 'lineMessageContent',
63
+ WEBPUSH: 'webPushMessageContent',
64
+ RCS: 'rcsMessageContent',
65
+ };
66
+
67
+ // contentItem.templateData is built by the legacy CreativesContainer content
68
+ // editor (messageMeta-oriented field names) — for channels whose legacy field
69
+ // names don't match CCS's own content schema, transform before sending rather
70
+ // than dumping templateData as-is. EMAIL: legacy stores the subject as
71
+ // `emailSubject` and the HTML body as `emailBody` (see
72
+ // extractContentForPreview.js and CreativesContainer/index.js, where the Bee
73
+ // editor's output is written back into templateData.emailBody — also
74
+ // mirrored by the existing legacy emailMessageContent builder in
75
+ // utils/transformerUtils.js). CCS expects `messageSubject`/`messageBody`.
76
+ export const CCS_CONTENT_TRANSFORMS = {
77
+ EMAIL: (templateData = {}) => ({
78
+ messageSubject: templateData.emailSubject,
79
+ messageBody: templateData.emailBody || templateData.emailHtml,
80
+ }),
81
+ // SMS: legacy stores the typed text as `messageBody` (see
82
+ // CreativesContainer/index.js's getCreativesData SMS case, and the existing
83
+ // legacy smsMessageContent builder in utils/transformerUtils.js, both of
84
+ // which read/write this same field) — CCS's schema expects it as `message`
85
+ // (confirmed against a real CCS response showing smsMessageContent.message
86
+ // stayed null when messageBody was sent instead).
87
+ SMS: (templateData = {}) => ({
88
+ message: templateData.messageBody || '',
89
+ }),
90
+ // WEBPUSH: the legacy editor nests the real data under
91
+ // messageContent.content (see CommunicationFlowCard.js's own comment on
92
+ // this same wrapping, for the edit-mode case) — extracting it flat here
93
+ // matches CCS's shape ({ messageSubject, accountId, content }); `offers`
94
+ // (a legacy-only field) is deliberately dropped, not part of CCS's schema.
95
+ WEBPUSH: (templateData = {}) => {
96
+ const inner = templateData.messageContent?.content || templateData;
97
+ return {
98
+ messageSubject: inner.messageSubject,
99
+ accountId: inner.accountId,
100
+ content: inner.content,
101
+ };
102
+ },
103
+ };
104
+
105
+ export const CCS_CHANNEL_DELIVERY_KEY_MAP = {
106
+ SMS: 'smsDeliverySettings',
107
+ EMAIL: 'emailDeliverySettings',
108
+ WHATSAPP: 'whatsappDeliverySettings',
109
+ PUSH: 'mpushDeliverySettings',
110
+ INAPP: 'inAppDeliverySettings',
111
+ ANDROID: 'androidDeliverySettings',
112
+ IOS: 'iosDeliverySettings',
113
+ ZALO: 'zaloDeliverySettings',
114
+ VIBER: 'viberDeliverySettings',
115
+ LINE: 'lineDeliverySettings',
116
+ WEBPUSH: 'webPushDeliverySettings',
117
+ RCS: 'rcsDeliverySettings',
118
+ };
119
+
120
+ // The UI's own internal channel identifier for mobile push is 'MOBILEPUSH'
121
+ // (see CreativesContainer/constants.js MOBILE_PUSH), but CCS's channel enum
122
+ // value is 'PUSH' (verified against real create responses — the content/
123
+ // delivery KEYS still say 'mpush*', only the enum VALUE is 'PUSH') —
124
+ // normalize before building the CCS payload. Every other channel identifier
125
+ // matches CCS as-is.
126
+ export const CCS_CHANNEL_NAME_MAP = {
127
+ MOBILEPUSH: 'PUSH',
128
+ };
129
+
41
130
  // Channel config - single source of truth for CreativesContainer and TemplatesV2
42
131
  // paneKey: TemplatesV2 defaultPanes object key (for channelsToHide)
43
132
  // channelProp: CreativesContainer channel prop (must match pane.key for tab to be active)
@@ -232,4 +321,4 @@ export const INCENTIVE_TYPES = [
232
321
  {
233
322
  value: 'badges', label: <FormattedMessage {...messages.badges} />, isActive: false, disabled: false,
234
323
  },
235
- ];
324
+ ];
@@ -9,7 +9,6 @@
9
9
  import React from 'react';
10
10
  import PropTypes from 'prop-types';
11
11
  import CommunicationFlow from './CommunicationFlow';
12
- import { hasSupportEngagementModule } from '../../utils/common';
13
12
 
14
13
  const CommunicationFlowContainer = ({
15
14
  config,
@@ -18,22 +17,16 @@ const CommunicationFlowContainer = ({
18
17
  onCancel,
19
18
  onChange,
20
19
  ...otherProps
21
- }) => {
22
- if (!hasSupportEngagementModule()) {
23
- return null;
24
- }
25
-
26
- return (
27
- <CommunicationFlow
28
- config={config}
29
- initialData={initialData}
30
- onSave={onSave}
31
- onCancel={onCancel}
32
- onChange={onChange}
33
- {...otherProps}
34
- />
35
- );
36
- };
20
+ }) => (
21
+ <CommunicationFlow
22
+ config={config}
23
+ initialData={initialData}
24
+ onSave={onSave}
25
+ onCancel={onCancel}
26
+ onChange={onChange}
27
+ {...otherProps}
28
+ />
29
+ );
37
30
 
38
31
  CommunicationFlowContainer.propTypes = {
39
32
  config: PropTypes.shape({
@@ -87,11 +80,13 @@ CommunicationFlowContainer.propTypes = {
87
80
  controls: PropTypes.array,
88
81
  }),
89
82
  }),
90
- context: PropTypes.object, // ouId, campaignId, programId, etc.
91
- useCCS: PropTypes.bool, // If false, skips CCS bulk-claim-approve on save. Defaults to true.
83
+ context: PropTypes.object, // ouId, campaignId, programId; name (required for CCS create),
84
+ // referenceId (optional, auto-generated from name if absent), description (optional).
85
+ useCCS: PropTypes.bool, // If false, skips the CCS createCommDefinition call on save. Defaults to true.
92
86
  }).isRequired,
93
87
  initialData: PropTypes.object, // for edit/preview mode
94
- onSave: PropTypes.func.isRequired, // (data) => void - called when user saves
88
+ onSave: PropTypes.func.isRequired, // (data) => void - called when user saves; data.ccsCommDefinition
89
+ // ({id, referenceId, version, status}) is present when the CCS create succeeded
95
90
  onCancel: PropTypes.func.isRequired, // () => void - called when user cancels
96
91
  onChange: PropTypes.func, // (data) => void - optional, called on data changes
97
92
  };
@@ -363,4 +363,8 @@ export default {
363
363
  id: `${prefix}.senderNotConfiguredError`,
364
364
  defaultMessage: 'Selected domain gateway id is not correct. Please change the domain id or contact the gateway team to register them with Capillary.',
365
365
  },
366
+ duplicateReferenceIdError: {
367
+ id: `${prefix}.duplicateReferenceIdError`,
368
+ defaultMessage: 'This reference ID is already in use in your organization. Please use a different reference ID.',
369
+ },
366
370
  };
@@ -29,6 +29,7 @@ import {
29
29
  getEmailDefaultsForDomainId,
30
30
  getSmsDefaultsForDomainId,
31
31
  getRcsAccountName,
32
+ getEmailDomainGatewayMapIdByDomainId,
32
33
  } from './deliverySettingsConfig';
33
34
  import messages from '../../messages';
34
35
  import './SenderDetails.scss';
@@ -151,6 +152,11 @@ const SenderDetails = ({
151
152
  saveValues.whatsappDomainId = matchedDomain?.domainProperties?.id ?? matchedDomain?.domainId ?? matchedDomain?.id;
152
153
  }
153
154
  }
155
+ // Include EMAIL domainGatewayMapId derived from the selected domain for persistence -
156
+ // CCS needs it alongside domainId, but it's not a user-facing field.
157
+ if (entity && fieldValues.emailDomain != null && fieldValues.emailDomain !== '') {
158
+ saveValues.emailDomainGatewayMapId = getEmailDomainGatewayMapIdByDomainId(entity, fieldValues.emailDomain);
159
+ }
154
160
  onSave(saveValues);
155
161
  }
156
162
  onClose();
@@ -83,6 +83,7 @@ const ENTITIES = {
83
83
  emailTwoDomains: {
84
84
  EMAIL: [
85
85
  {
86
+ id: 'gwmap-a',
86
87
  domainProperties: {
87
88
  id: 'em-a',
88
89
  domainName: 'Mail Alpha',
@@ -101,6 +102,7 @@ const ENTITIES = {
101
102
  },
102
103
  },
103
104
  {
105
+ id: 'gwmap-b',
104
106
  domainProperties: {
105
107
  id: 'em-b',
106
108
  domainName: 'Mail Beta',
@@ -322,6 +324,29 @@ describe('SenderDetails', () => {
322
324
  });
323
325
  });
324
326
 
327
+ it('persists emailDomainGatewayMapId derived from the selected email domain on save', async () => {
328
+ const { onSave } = renderSenderDetails({
329
+ channels: ['EMAIL'],
330
+ preloadedDomainProperties: ENTITIES.emailTwoDomains,
331
+ });
332
+ const root = document.querySelector('.sender-details');
333
+ await waitFor(() => expect(within(root).getByText('Mail Alpha')).toBeInTheDocument());
334
+
335
+ await openSelectAndChoose(root, 0, 'Mail Beta');
336
+ await waitFor(() => expect(within(root).getByText('beta@other.com')).toBeInTheDocument());
337
+
338
+ const saveBtn = within(root).getByRole('button', { name: /save changes/i });
339
+ await waitFor(() => expect(saveBtn).not.toBeDisabled());
340
+ fireEvent.click(saveBtn);
341
+
342
+ expect(onSave).toHaveBeenCalledWith(
343
+ expect.objectContaining({
344
+ emailDomain: 'em-b',
345
+ emailDomainGatewayMapId: 'gwmap-b',
346
+ }),
347
+ );
348
+ });
349
+
325
350
  it('resets email domain row back to the first gateway', async () => {
326
351
  renderSenderDetails({
327
352
  channels: ['EMAIL'],
@@ -438,6 +438,7 @@ describe('deliverySettingsConfig — user-facing flows', () => {
438
438
  smsDomain: 'd1',
439
439
  smsSenderId: 'g1',
440
440
  emailDomain: 'ed',
441
+ emailDomainGatewayMapId: 'gwmap-1',
441
442
  emailSenderId: 'mail@x.com',
442
443
  emailSenderName: 'N',
443
444
  emailReplyToId: 'r@x.com',
@@ -455,9 +456,10 @@ describe('deliverySettingsConfig — user-facing flows', () => {
455
456
  SMS: { domainId: 'd1', gsmSenderId: 'g1' },
456
457
  EMAIL: {
457
458
  domainId: 'ed',
458
- senderEmail: 'mail@x.com',
459
+ domainGatewayMapId: 'gwmap-1',
460
+ senderId: 'mail@x.com',
459
461
  senderLabel: 'N',
460
- senderReplyTo: 'r@x.com',
462
+ replyToId: 'r@x.com',
461
463
  },
462
464
  WHATSAPP: { domainId: 'wd', senderMobNum: '+w' },
463
465
  RCS: { senderMobNum: '+r', rcsSender: '+r' },
@@ -472,9 +474,10 @@ describe('deliverySettingsConfig — user-facing flows', () => {
472
474
  SMS: { domainId: 'd', gsmSenderId: 'g' },
473
475
  EMAIL: {
474
476
  domainId: 'ed',
475
- senderEmail: 'e@e.com',
477
+ domainGatewayMapId: 'gwmap-2',
478
+ senderId: 'e@e.com',
476
479
  senderLabel: 'L',
477
- senderReplyTo: 'r@e.com',
480
+ replyToId: 'r@e.com',
478
481
  },
479
482
  WHATSAPP: { senderMobNum: '+w', domainId: 'wid' },
480
483
  RCS: { rcsSender: '+r' },
@@ -486,6 +489,10 @@ describe('deliverySettingsConfig — user-facing flows', () => {
486
489
  smsDomain: 'd',
487
490
  smsSenderId: 'g',
488
491
  emailDomain: 'ed',
492
+ emailDomainGatewayMapId: 'gwmap-2',
493
+ emailSenderId: 'e@e.com',
494
+ emailSenderName: 'L',
495
+ emailReplyToId: 'r@e.com',
489
496
  whatsappDomainId: 'wid',
490
497
  viberSenderId: 'vx',
491
498
  });
@@ -181,6 +181,14 @@ const getEmailDomainValue = (entity) => {
181
181
  return emailData.domainProperties?.id ?? emailData.domainId ?? emailData.id ?? '';
182
182
  };
183
183
 
184
+ // CCS's domainGatewayMapId is the domainProperties API's own outer `id`
185
+ // (distinct from `domainProperties.id`, which is the numeric domainId) - the
186
+ // domain-gateway-mapping id for the selected domain entry.
187
+ const getEmailDomainGatewayMapIdByDomainId = (entity, domainId) => {
188
+ const domainEntry = getEmailDomainById(entity, domainId);
189
+ return domainEntry?.id ?? '';
190
+ };
191
+
184
192
  /** Domain name for summary display (parseEntityForDisplay) */
185
193
  const getEmailDomainNameForDisplay = (entity) => {
186
194
  const emailData = getEmailData(entity);
@@ -262,6 +270,8 @@ const getEmailDefaultsForDomain = (domainData) => {
262
270
  * context.fieldValues has current form state (e.g. selected emailDomain)
263
271
  * Aligned with adiona-ui/cap-campaigns-v2: default = contactInfo item with default:true, or first
264
272
  */
273
+ export { getEmailDomainGatewayMapIdByDomainId };
274
+
265
275
  export const getDefaultValueForField = (fieldKey, entity, context = {}) => {
266
276
  if (!entity) return '';
267
277
  const fieldValues = context?.fieldValues || {};
@@ -574,7 +584,11 @@ export const parseEntityForDisplay = (entity, context = {}) => {
574
584
  senderDetails: entity,
575
585
  smsSenderId: getSmsSenderIdValue(entity),
576
586
  smsDomain: getSmsDomainValue(entity),
587
+ // emailDomain here is the domain NAME (for display only) - buildChannelSettingFromFieldValues
588
+ // needs the numeric domainId/domainGatewayMapId, sourced separately below.
577
589
  emailDomain: getEmailDomainNameForDisplay(entity),
590
+ emailDomainId: getEmailDomainValue(entity),
591
+ emailDomainGatewayMapId: getEmailDomainGatewayMapIdByDomainId(entity, getEmailDomainValue(entity)),
578
592
  emailSenderId: getEmailSenderIdValue(entity),
579
593
  viberSenderId: getSenderIdFromContactInfo(entity, 'VIBER'),
580
594
  zaloSenderId: getSenderIdFromContactInfo(entity, 'ZALO'),
@@ -600,10 +614,14 @@ export const buildChannelSettingFromFieldValues = (fieldValues = {}) => {
600
614
  }
601
615
  if (fieldValues.emailDomain != null || fieldValues.emailSenderId != null || fieldValues.emailSenderName != null || fieldValues.emailReplyToId != null) {
602
616
  channelSetting.EMAIL = {
603
- domainId: fieldValues.emailDomain,
604
- senderEmail: fieldValues.emailSenderId,
617
+ // emailDomainId (numeric, from the auto-save-defaults path via parseEntityForDisplay)
618
+ // takes priority over emailDomain, which is numeric when sourced from SenderDetails'
619
+ // own field state but a display NAME string when sourced from parseEntityForDisplay.
620
+ domainId: fieldValues.emailDomainId ?? fieldValues.emailDomain,
621
+ domainGatewayMapId: fieldValues.emailDomainGatewayMapId,
622
+ senderId: fieldValues.emailSenderId,
605
623
  senderLabel: fieldValues.emailSenderName,
606
- senderReplyTo: fieldValues.emailReplyToId,
624
+ replyToId: fieldValues.emailReplyToId,
607
625
  };
608
626
  }
609
627
  if (fieldValues.whatsappSenderNumber != null || fieldValues.whatsappAccount != null) {
@@ -673,9 +691,10 @@ export const parseChannelSettingForDisplay = (channelSetting = {}) => {
673
691
  }
674
692
  if (channelSetting.EMAIL) {
675
693
  out.emailDomain = channelSetting.EMAIL.domainId;
676
- out.emailSenderId = channelSetting.EMAIL.senderEmail;
694
+ out.emailDomainGatewayMapId = channelSetting.EMAIL.domainGatewayMapId;
695
+ out.emailSenderId = channelSetting.EMAIL.senderId;
677
696
  out.emailSenderName = channelSetting.EMAIL.senderLabel;
678
- out.emailReplyToId = channelSetting.EMAIL.senderReplyTo;
697
+ out.emailReplyToId = channelSetting.EMAIL.replyToId;
679
698
  }
680
699
  if (channelSetting.WHATSAPP) {
681
700
  out.whatsappSenderNumber = channelSetting.WHATSAPP.senderMobNum;
@@ -2456,7 +2456,7 @@ export class Creatives extends React.Component {
2456
2456
  )}
2457
2457
  handleClose={this.handleCloseSlideBox}
2458
2458
  show={showSlideBox}
2459
- size="size-xl"
2459
+ size="size-l"
2460
2460
  />
2461
2461
  </SlideBoxWrapper>
2462
2462
  );