@capillarytech/creatives-library 9.0.64 → 9.0.65

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.64",
4
+ "version": "9.0.65",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -334,6 +334,16 @@ export default {
334
334
  id: `${prefix}.rcsAccountLabel`,
335
335
  defaultMessage: 'RCS account',
336
336
  },
337
+ /** RCS: SMS fallback domain (slidebox) — shown when the RCS content has SMS-fallback content configured */
338
+ fallbackSmsDomainLabel: {
339
+ id: `${prefix}.fallbackSmsDomainLabel`,
340
+ defaultMessage: 'Fallback SMS domain',
341
+ },
342
+ /** RCS: SMS fallback sender ID (slidebox + summary) */
343
+ fallbackSmsSenderIdLabel: {
344
+ id: `${prefix}.fallbackSmsSenderIdLabel`,
345
+ defaultMessage: 'Fallback SMS sender ID',
346
+ },
337
347
  // Dynamic Controls toggle labels and descriptions
338
348
  sendToControlCustomers: {
339
349
  id: `${prefix}.sendToControlCustomers`,
@@ -20,13 +20,15 @@ import {
20
20
  buildChannelSettingFromFieldValues,
21
21
  parseChannelSettingForDisplay,
22
22
  getWhatsappAccountName,
23
+ hasRcsSmsFallbackContent,
24
+ RCS_SMS_FALLBACK_CHANNEL,
23
25
  } from './deliverySettingsConfig';
24
26
  import { getDomainProperties, fetchWeCrmAccounts } from '../../../../services/api';
25
27
  import { loadItem } from '../../../../services/localStorageApi';
26
28
  import { CHANNELS_WITHOUT_DELIVERY } from '../../constants';
27
29
  import messages from '../../messages';
28
30
  import './DeliverySettingsSection.scss';
29
- import { WHATSAPP } from "../../../CreativesContainer/constants";
31
+ import { WHATSAPP, SMS, RCS } from "../../../CreativesContainer/constants";
30
32
 
31
33
  const DeliverySettingsSection = ({
32
34
  contentItems = [],
@@ -60,6 +62,21 @@ const DeliverySettingsSection = ({
60
62
  [contentChannels],
61
63
  );
62
64
 
65
+ // RCS SMS-fallback sender ID uses SMS domain data even when SMS isn't a selected channel.
66
+ const hasRcsSmsFallback = useMemo(() => hasRcsSmsFallbackContent(contentItems), [contentItems]);
67
+
68
+ // Fetch SMS domain properties for fallback sender options without adding SMS as a displayed channel.
69
+ const deliveryFetchChannels = useMemo(() => {
70
+ if (!hasRcsSmsFallback || deliveryChannels.includes(SMS)) return deliveryChannels;
71
+ return [...deliveryChannels, SMS];
72
+ }, [deliveryChannels, hasRcsSmsFallback]);
73
+
74
+ // Include RCS SMS-fallback fields via a pseudo-channel without rendering an SMS section.
75
+ const senderDetailsChannels = useMemo(() => {
76
+ if (!hasRcsSmsFallback) return deliveryChannels;
77
+ return [...deliveryChannels, RCS_SMS_FALLBACK_CHANNEL];
78
+ }, [deliveryChannels, hasRcsSmsFallback]);
79
+
63
80
  // Hide section when all configured channels are MPUSH, INAPP, WEBPUSH
64
81
  const shouldShow = useMemo(() => {
65
82
  if (contentChannels.length === 0) return false;
@@ -79,11 +96,11 @@ const DeliverySettingsSection = ({
79
96
  }, [contentItems]);
80
97
 
81
98
  // Stable key for deduplication - avoid re-fetch when array reference changes
82
- const deliveryChannelKey = deliveryChannels.slice().sort().join(',');
99
+ const deliveryChannelKey = deliveryFetchChannels.slice().sort().join(',');
83
100
  const deliveryEnabled = !!deliverySettingsData;
84
101
  // Fetch domainProperties when content is configured (single source, deduplicated)
85
102
  useEffect(() => {
86
- if (!deliveryEnabled || deliveryChannels.length === 0) {
103
+ if (!deliveryEnabled || deliveryFetchChannels.length === 0) {
87
104
  setDomainPropertiesData(null);
88
105
  lastChannelKeyRef.current = '';
89
106
  return undefined;
@@ -102,7 +119,7 @@ const DeliverySettingsSection = ({
102
119
  // uses) so both screens fetch domains for the same org unit; fall back to the logged-in
103
120
  // user's own cached OU only when a consumer hasn't supplied one.
104
121
  const resolvedOrgUnitId = orgUnitId ?? (loadItem('ouId') || loadItem('orgID'));
105
- const response = await getDomainProperties(deliveryChannels, resolvedOrgUnitId);
122
+ const response = await getDomainProperties(deliveryFetchChannels, resolvedOrgUnitId);
106
123
  if (!cancelled) {
107
124
  const raw = response?.entity || response;
108
125
  // Normalize channel keys to uppercase (API may return Viber, viber, etc.)
@@ -211,7 +228,12 @@ const DeliverySettingsSection = ({
211
228
  let didChange = false;
212
229
 
213
230
  deliveryChannels.forEach((channel) => {
214
- const apiDataForChannel = channelSettingFromAPI[channel];
231
+ let apiDataForChannel = channelSettingFromAPI[channel];
232
+ // Show RCS SMS-fallback fields only when fallback content is configured, not merely when SMS is selected.
233
+ if (channel === RCS && !hasRcsSmsFallback && apiDataForChannel) {
234
+ const { smsFallbackDomainId, smsFallbackSenderId, ...rcsWithoutFallback } = apiDataForChannel;
235
+ apiDataForChannel = rcsWithoutFallback;
236
+ }
215
237
  const userSavedDataForChannel = savedChannelSetting[channel];
216
238
 
217
239
  const apiHasRealSenderId = hasRealData(apiDataForChannel);
@@ -300,6 +322,16 @@ const DeliverySettingsSection = ({
300
322
  </CapRow>
301
323
  ));
302
324
  })}
325
+ {hasRcsSmsFallback && parsedSenderDetails?.rcsSmsFallbackSenderId && (
326
+ <CapRow useLegacy key="RCS-rcsSmsFallbackSenderId" type="flex" align="middle" justify="space-between" className="delivery-settings-section__field-row">
327
+ <CapHeading type="label4" className="delivery-settings-section__label">
328
+ {formatMessage(messages.fallbackSmsSenderIdLabel)}
329
+ </CapHeading>
330
+ <CapLabel type="label9" className="delivery-settings-section__value" title={parsedSenderDetails.rcsSmsFallbackSenderId}>
331
+ {parsedSenderDetails.rcsSmsFallbackSenderId}
332
+ </CapLabel>
333
+ </CapRow>
334
+ )}
303
335
  </CapRow>
304
336
  <CapIcon
305
337
  type="chevron-right"
@@ -313,7 +345,7 @@ const DeliverySettingsSection = ({
313
345
  <SenderDetails
314
346
  show={showSlidebox}
315
347
  onClose={() => setShowSlidebox(false)}
316
- channels={deliveryChannels}
348
+ channels={senderDetailsChannels}
317
349
  preloadedDomainProperties={entityWithWecrmViber}
318
350
  isLoadingDomainProperties={isFetchingDomains}
319
351
  savedFieldValues={parseChannelSettingForDisplay(deliverySetting?.channelSetting)}
@@ -30,6 +30,7 @@ import {
30
30
  getSmsDefaultsForDomainId,
31
31
  getRcsAccountName,
32
32
  getEmailDomainGatewayMapIdByDomainId,
33
+ getRcsSmsFallbackDefaultDomain,
33
34
  } from './deliverySettingsConfig';
34
35
  import messages from '../../messages';
35
36
  import './SenderDetails.scss';
@@ -114,6 +115,10 @@ const SenderDetails = ({
114
115
  next.emailSenderName = defs.senderName;
115
116
  next.emailReplyToId = defs.replyToId;
116
117
  }
118
+ if (fieldKey === 'rcsSmsFallbackDomain' && entity) {
119
+ const defs = getSmsDefaultsForDomainId(entity, value);
120
+ next.rcsSmsFallbackSenderId = defs.senderId;
121
+ }
117
122
  return next;
118
123
  });
119
124
  }, [entity]);
@@ -133,6 +138,11 @@ const SenderDetails = ({
133
138
  next.emailSenderId = defs.senderId;
134
139
  next.emailSenderName = defs.senderName;
135
140
  next.emailReplyToId = defs.replyToId;
141
+ } else if (fieldKey === 'rcsSmsFallbackDomain') {
142
+ const defaultDomain = getRcsSmsFallbackDefaultDomain(entity);
143
+ const defs = getSmsDefaultsForDomainId(entity, defaultDomain);
144
+ next.rcsSmsFallbackDomain = defaultDomain;
145
+ next.rcsSmsFallbackSenderId = defs.senderId;
136
146
  } else {
137
147
  next[fieldKey] = getDefaultValueForField(fieldKey, entity, { fieldValues: prev });
138
148
  }
@@ -238,7 +248,7 @@ const SenderDetails = ({
238
248
 
239
249
  if (type === FIELD_TYPE.SELECT) {
240
250
  const isSingleOption = options.length <= 1;
241
- const isDomainField = fieldKey === 'smsDomain' || fieldKey === 'emailDomain';
251
+ const isDomainField = fieldKey === 'smsDomain' || fieldKey === 'emailDomain' || fieldKey === 'rcsSmsFallbackDomain';
242
252
  const showEmptyOptionsError = !isDisabled && entity && options?.length === 0;
243
253
  const emptyOptionsErrorMsg = isDomainField
244
254
  ? formatMessage(messages.domainGatewayError)
@@ -598,6 +598,100 @@ describe('DeliverySettingsSection — marketer flows', () => {
598
598
  });
599
599
  });
600
600
 
601
+ describe('RCS SMS-fallback sender ID (normal-flow parity)', () => {
602
+ const rcsWithFallbackContentItem = {
603
+ channel: 'RCS',
604
+ templateData: {
605
+ smsFallBackContent: { message: 'Fallback SMS body' },
606
+ },
607
+ };
608
+
609
+ it('fetches SMS domains too (in addition to RCS) when the RCS content has SMS-fallback content', async () => {
610
+ getDomainProperties.mockResolvedValue({ entity: { ...apiEntity.RCS, ...apiEntity.SMS } });
611
+
612
+ renderSection({ contentItems: [rcsWithFallbackContentItem] });
613
+
614
+ await waitFor(() => expect(getDomainProperties).toHaveBeenCalledWith(['RCS', 'SMS'], 'test-ou'));
615
+ });
616
+
617
+ it('does not fetch SMS domains when the RCS content has no SMS-fallback content', async () => {
618
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.RCS });
619
+
620
+ renderSection({ contentItems: [{ channel: 'RCS' }] });
621
+
622
+ await waitFor(() => expect(getDomainProperties).toHaveBeenCalledWith(['RCS'], 'test-ou'));
623
+ });
624
+
625
+ it('shows a "Fallback SMS sender ID" summary row sourced from the SMS domain default sender', async () => {
626
+ getDomainProperties.mockResolvedValue({ entity: { ...apiEntity.RCS, ...apiEntity.SMS } });
627
+
628
+ renderSection({ contentItems: [rcsWithFallbackContentItem] });
629
+
630
+ await waitFor(() => {
631
+ expect(screen.getByText('Fallback SMS sender ID')).toBeInTheDocument();
632
+ expect(screen.getByText('+1002003001')).toBeInTheDocument();
633
+ });
634
+ });
635
+
636
+ it('does not show the fallback summary row when the RCS content has no SMS-fallback content', async () => {
637
+ getDomainProperties.mockResolvedValue({ entity: apiEntity.RCS });
638
+
639
+ renderSection({ contentItems: [{ channel: 'RCS' }] });
640
+
641
+ await waitFor(() => expect(screen.getByText('Sender details')).toBeInTheDocument());
642
+ expect(screen.queryByText('Fallback SMS sender ID')).not.toBeInTheDocument();
643
+ });
644
+
645
+ it('lets the marketer pick a fallback SMS sender ID in the slidebox and persists it under channelSetting.RCS', async () => {
646
+ getDomainProperties.mockResolvedValue({ entity: { ...apiEntity.RCS, ...apiEntity.SMS } });
647
+ const onDeliverySettingChange = jest.fn();
648
+
649
+ renderSection({ contentItems: [rcsWithFallbackContentItem], onDeliverySettingChange });
650
+
651
+ await waitFor(() => expect(screen.getByText('Fallback SMS sender ID')).toBeInTheDocument());
652
+ onDeliverySettingChange.mockClear();
653
+ await openSenderDetailsFromSummary();
654
+
655
+ const slidebox = document.querySelector('.sender-details');
656
+ expect(within(slidebox).getByText('Fallback SMS sender ID')).toBeInTheDocument();
657
+
658
+ const combos = within(slidebox).getAllByRole('combobox');
659
+ // RCS domain, RCS sender ID, Fallback SMS domain, Fallback SMS sender ID
660
+ await userEvent.click(combos[combos.length - 1]);
661
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
662
+ await userEvent.click(await findVisibleSelectOption('+1002003002'));
663
+
664
+ const saveBtn = within(slidebox).getByRole('button', { name: /save changes/i });
665
+ await waitFor(() => expect(saveBtn).not.toBeDisabled());
666
+ await userEvent.click(saveBtn);
667
+
668
+ expect(onDeliverySettingChange).toHaveBeenCalled();
669
+ const payload = onDeliverySettingChange.mock.calls[0][0];
670
+ expect(payload.channelSetting.RCS).toMatchObject({
671
+ smsFallbackSenderId: '+1002003002',
672
+ });
673
+ });
674
+
675
+ it('strips smsFallbackDomainId/smsFallbackSenderId from the auto-saved RCS entry when the RCS content has no SMS-fallback content, even though SMS domain data is present (SMS also selected independently)', async () => {
676
+ // entity.SMS leaks rcsSmsFallbackDomain/rcsSmsFallbackSenderId into channelSettingFromAPI.RCS
677
+ // via parseSenderDetailsFromEntity, regardless of hasRcsSmsFallback — the auto-save effect
678
+ // must strip them back out for RCS when there's no actual fallback content configured.
679
+ getDomainProperties.mockResolvedValue({ entity: { ...apiEntity.RCS, ...apiEntity.SMS } });
680
+ const onDeliverySettingChange = jest.fn();
681
+
682
+ renderSection({
683
+ contentItems: [{ channel: 'RCS' }, { channel: 'SMS' }],
684
+ onDeliverySettingChange,
685
+ });
686
+
687
+ await waitFor(() => expect(onDeliverySettingChange).toHaveBeenCalled());
688
+ const payload = onDeliverySettingChange.mock.calls[0][0];
689
+ expect(payload.channelSetting.RCS).not.toHaveProperty('smsFallbackDomainId');
690
+ expect(payload.channelSetting.RCS).not.toHaveProperty('smsFallbackSenderId');
691
+ expect(payload.channelSetting.RCS).toMatchObject({ senderMobNum: '+12025550123' });
692
+ });
693
+ });
694
+
601
695
  describe('DeduplicationRef — skips re-fetch when data already loaded for the same channels', () => {
602
696
  it('does not issue a second network call when the same channels are re-rendered with the same deliverySettingsData', async () => {
603
697
  // Covers line 85-86: lastChannelKeyRef === deliveryChannelKey && domainPropertiesData already set
@@ -448,6 +448,85 @@ describe('SenderDetails', () => {
448
448
  });
449
449
  });
450
450
 
451
+ it('renders the RCS SMS-fallback domain + sender ID fields when RCS_SMS_FALLBACK_CHANNEL is included', async () => {
452
+ renderSenderDetails({
453
+ channels: ['RCS', deliverySettingsConfig.RCS_SMS_FALLBACK_CHANNEL],
454
+ preloadedDomainProperties: { ...ENTITIES.rcsBrand, ...ENTITIES.smsTwoSenders },
455
+ });
456
+ const root = document.querySelector('.sender-details');
457
+ await waitFor(() => {
458
+ expect(within(root).getByText('RCS Brand Co')).toBeInTheDocument();
459
+ expect(within(root).getByText('Fallback SMS domain')).toBeInTheDocument();
460
+ expect(within(root).getByText('Fallback SMS sender ID')).toBeInTheDocument();
461
+ expect(within(root).getByText('+1002003001')).toBeInTheDocument();
462
+ });
463
+ });
464
+
465
+ it('does not render the RCS SMS-fallback fields when the pseudo-channel is not included', async () => {
466
+ renderSenderDetails({
467
+ channels: ['RCS'],
468
+ preloadedDomainProperties: { ...ENTITIES.rcsBrand, ...ENTITIES.smsTwoSenders },
469
+ });
470
+ const root = document.querySelector('.sender-details');
471
+ await waitFor(() => expect(within(root).getByText('RCS Brand Co')).toBeInTheDocument());
472
+ expect(within(root).queryByText('Fallback SMS domain')).not.toBeInTheDocument();
473
+ });
474
+
475
+ it('changing the fallback SMS domain auto-populates the fallback sender ID', async () => {
476
+ renderSenderDetails({
477
+ channels: ['RCS', deliverySettingsConfig.RCS_SMS_FALLBACK_CHANNEL],
478
+ preloadedDomainProperties: { ...ENTITIES.rcsBrand, ...ENTITIES.smsTwoDomains },
479
+ });
480
+ const root = document.querySelector('.sender-details');
481
+ await waitFor(() => expect(within(root).getByText('Gateway A')).toBeInTheDocument());
482
+
483
+ // Combo order for ['RCS', RCS_SMS_FALLBACK_CHANNEL]: rcsAccount (disabled), fallback domain, fallback sender ID.
484
+ await openSelectAndChoose(root, 1, 'Gateway B');
485
+ await waitFor(() => expect(within(root).getByText('+222')).toBeInTheDocument());
486
+ });
487
+
488
+ it('resets the fallback SMS domain + sender ID back to the first SMS domain', async () => {
489
+ renderSenderDetails({
490
+ channels: ['RCS', deliverySettingsConfig.RCS_SMS_FALLBACK_CHANNEL],
491
+ preloadedDomainProperties: { ...ENTITIES.rcsBrand, ...ENTITIES.smsTwoDomains },
492
+ });
493
+ const root = document.querySelector('.sender-details');
494
+ await waitFor(() => expect(within(root).getByText('Gateway A')).toBeInTheDocument());
495
+
496
+ await openSelectAndChoose(root, 1, 'Gateway B');
497
+ await waitFor(() => expect(within(root).getByText('+222')).toBeInTheDocument());
498
+
499
+ const resetLinks = within(root).getAllByText('Reset');
500
+ fireEvent.click(resetLinks[resetLinks.length - 2]);
501
+
502
+ await waitFor(() => {
503
+ expect(within(root).getByText('Gateway A')).toBeInTheDocument();
504
+ expect(within(root).getByText('+111')).toBeInTheDocument();
505
+ });
506
+ });
507
+
508
+ it('persists the fallback SMS sender ID under rcsSmsFallbackSenderId on save', async () => {
509
+ const { onSave } = renderSenderDetails({
510
+ channels: ['RCS', deliverySettingsConfig.RCS_SMS_FALLBACK_CHANNEL],
511
+ preloadedDomainProperties: { ...ENTITIES.rcsBrand, ...ENTITIES.smsTwoSenders },
512
+ });
513
+ const root = document.querySelector('.sender-details');
514
+ await waitFor(() => expect(within(root).getByText('+1002003001')).toBeInTheDocument());
515
+
516
+ const combos = within(root).getAllByRole('combobox');
517
+ fireEvent.mouseDown(combos[combos.length - 1]);
518
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
519
+ fireEvent.click(await findVisibleSelectOption('+1002003002', WAIT_OPTIONS));
520
+
521
+ const saveBtn = within(root).getByRole('button', { name: /save changes/i });
522
+ await waitFor(() => expect(saveBtn).not.toBeDisabled());
523
+ fireEvent.click(saveBtn);
524
+
525
+ expect(onSave).toHaveBeenCalledWith(
526
+ expect.objectContaining({ rcsSmsFallbackSenderId: '+1002003002' }),
527
+ );
528
+ });
529
+
451
530
  it('exposes parseSenderDetailsFromEntity aligned with parseEntityForDisplay', () => {
452
531
  expect(parseSenderDetailsFromEntity(ENTITIES.smsOk)).toEqual(
453
532
  parseEntityForDisplay(ENTITIES.smsOk, {}),
@@ -25,6 +25,10 @@ import {
25
25
  buildChannelSettingFromFieldValues,
26
26
  hasDomainGateway,
27
27
  parseChannelSettingForDisplay,
28
+ RCS_SMS_FALLBACK_CHANNEL,
29
+ hasRcsSmsFallbackContent,
30
+ getRcsSmsFallbackDefaultDomain,
31
+ getRcsSmsFallbackBodyText,
28
32
  } from '../deliverySettingsConfig';
29
33
 
30
34
  /** Typical campaigns `domainProperties` entity: channel keys → list of gateway rows */
@@ -1001,6 +1005,110 @@ describe('deliverySettingsConfig — user-facing flows', () => {
1001
1005
  });
1002
1006
  });
1003
1007
 
1008
+ describe('RCS SMS-fallback sender ID (normal-flow parity)', () => {
1009
+ it('detects meaningful SMS-fallback content on the RCS contentItem (message field)', () => {
1010
+ expect(hasRcsSmsFallbackContent([
1011
+ { channel: 'RCS', templateData: { smsFallBackContent: { message: 'Hi {{1}}' } } },
1012
+ ])).toBe(true);
1013
+ });
1014
+
1015
+ it('recognizes the alternate smsContent/smsTemplateContent/content keys as meaningful fallback content', () => {
1016
+ expect(hasRcsSmsFallbackContent([
1017
+ { channel: 'RCS', templateData: { smsFallBackContent: { smsContent: 'Hi' } } },
1018
+ ])).toBe(true);
1019
+ expect(hasRcsSmsFallbackContent([
1020
+ { channel: 'RCS', templateData: { smsFallBackContent: { smsTemplateContent: 'Hi' } } },
1021
+ ])).toBe(true);
1022
+ expect(hasRcsSmsFallbackContent([
1023
+ { channel: 'RCS', templateData: { smsFallBackContent: { content: 'Hi' } } },
1024
+ ])).toBe(true);
1025
+ });
1026
+
1027
+ it('is false when there is no RCS content item, no smsFallBackContent, or it is only whitespace', () => {
1028
+ expect(hasRcsSmsFallbackContent([])).toBe(false);
1029
+ expect(hasRcsSmsFallbackContent([{ channel: 'SMS', templateData: {} }])).toBe(false);
1030
+ expect(hasRcsSmsFallbackContent([{ channel: 'RCS', templateData: {} }])).toBe(false);
1031
+ expect(hasRcsSmsFallbackContent([
1032
+ { channel: 'RCS', templateData: { smsFallBackContent: { message: ' ' } } },
1033
+ ])).toBe(false);
1034
+ expect(hasRcsSmsFallbackContent([
1035
+ { channel: 'RCS', templateData: { smsFallBackContent: null } },
1036
+ ])).toBe(false);
1037
+ });
1038
+
1039
+ it('getRcsSmsFallbackBodyText falls all the way through to "" when smsFallBackContent is an object with none of the recognized keys', () => {
1040
+ expect(getRcsSmsFallbackBodyText({})).toBe('');
1041
+ expect(getRcsSmsFallbackBodyText({ unrelatedField: 'x' })).toBe('');
1042
+ });
1043
+
1044
+ it('defaults contentItems to [] (no args) and tolerates a null/non-array contentItems', () => {
1045
+ expect(hasRcsSmsFallbackContent()).toBe(false);
1046
+ expect(hasRcsSmsFallbackContent(null)).toBe(false);
1047
+ });
1048
+
1049
+ it('is false when the RCS-shaped fallback content sits on an item with no channel key (falsy channel branch)', () => {
1050
+ expect(hasRcsSmsFallbackContent([
1051
+ { templateData: { smsFallBackContent: { message: 'Hi' } } },
1052
+ ])).toBe(false);
1053
+ });
1054
+
1055
+ it('exposes fallback domain + sender ID fields only when RCS_SMS_FALLBACK_CHANNEL is requested', () => {
1056
+ const withoutFallback = getFieldsForChannels(['RCS']).map((f) => f.fieldKey);
1057
+ expect(withoutFallback).not.toContain('rcsSmsFallbackDomain');
1058
+ expect(withoutFallback).not.toContain('rcsSmsFallbackSenderId');
1059
+
1060
+ const withFallback = getFieldsForChannels(['RCS', RCS_SMS_FALLBACK_CHANNEL]).map((f) => f.fieldKey);
1061
+ expect(withFallback).toEqual(expect.arrayContaining(['rcsSmsFallbackDomain', 'rcsSmsFallbackSenderId']));
1062
+ });
1063
+
1064
+ it('sources fallback sender ID options from SMS domain data, filtered by the selected fallback domain', () => {
1065
+ const senderField = getFieldsForChannels([RCS_SMS_FALLBACK_CHANNEL]).find((f) => f.fieldKey === 'rcsSmsFallbackSenderId');
1066
+ const opts = senderField.getOptions(domainPropertiesFromApi, { fieldValues: { rcsSmsFallbackDomain: 's2' } });
1067
+ expect(opts.map((o) => o.value)).toEqual(['+2002']);
1068
+ });
1069
+
1070
+ it('sources fallback sender ID options from the first SMS domain when no context is passed at all (default context)', () => {
1071
+ const senderField = getFieldsForChannels([RCS_SMS_FALLBACK_CHANNEL]).find((f) => f.fieldKey === 'rcsSmsFallbackSenderId');
1072
+ const opts = senderField.getOptions(domainPropertiesFromApi);
1073
+ expect(opts.map((o) => o.value)).toContain('+1001');
1074
+ });
1075
+
1076
+ it('defaults the fallback domain to the first SMS domain (for Reset)', () => {
1077
+ expect(getRcsSmsFallbackDefaultDomain(domainPropertiesFromApi)).toBe('s1');
1078
+ expect(getRcsSmsFallbackDefaultDomain(null)).toBe('');
1079
+ });
1080
+
1081
+ it('parseEntityForDisplay includes the fallback domain/sender ID sourced from SMS domain data', () => {
1082
+ const summary = parseEntityForDisplay(domainPropertiesFromApi);
1083
+ expect(summary.rcsSmsFallbackDomain).toBe('s1');
1084
+ expect(summary.rcsSmsFallbackSenderId).toBe('+1001');
1085
+ });
1086
+
1087
+ it('buildChannelSettingFromFieldValues nests fallback domain/sender ID under RCS alongside the RCS sender number', () => {
1088
+ const saved = buildChannelSettingFromFieldValues({
1089
+ rcsSenderNumber: '+r',
1090
+ rcsSmsFallbackDomain: 's2',
1091
+ rcsSmsFallbackSenderId: '+2002',
1092
+ });
1093
+ expect(saved).toEqual({
1094
+ RCS: {
1095
+ senderMobNum: '+r',
1096
+ rcsSender: '+r',
1097
+ smsFallbackDomainId: 's2',
1098
+ smsFallbackSenderId: '+2002',
1099
+ },
1100
+ });
1101
+ });
1102
+
1103
+ it('parseChannelSettingForDisplay hydrates the fallback domain/sender ID back from saved channelSetting', () => {
1104
+ const flat = parseChannelSettingForDisplay({
1105
+ RCS: { rcsSender: '+r', smsFallbackDomainId: 's2', smsFallbackSenderId: '+2002' },
1106
+ });
1107
+ expect(flat.rcsSmsFallbackDomain).toBe('s2');
1108
+ expect(flat.rcsSmsFallbackSenderId).toBe('+2002');
1109
+ });
1110
+ });
1111
+
1004
1112
  describe('getViberAccountOptions — synthetic fallback prepend', () => {
1005
1113
  it('prepends a synthetic entry when the saved viberAccount is absent from API options (stale saved domain)', () => {
1006
1114
  // Triggers the prepend path: context.fieldValues.viberAccount is not present in API options
@@ -120,14 +120,16 @@ const getSmsDomainValue = (entity) => {
120
120
  return smsData.domainProperties?.id ?? smsData.domainId ?? smsData.id ?? '';
121
121
  };
122
122
 
123
- // Sender ID - options only from selected domain (like Email)
124
- const getSmsSenderIdOptions = (entity, context = {}) => {
125
- const selectedDomainId = context?.fieldValues?.smsDomain;
123
+ // Sender ID options restricted to the selected domain; domainFieldKey supports alternate domain field keys.
124
+ const getSmsSenderIdOptionsForDomainField = (entity, context = {}, domainFieldKey = 'smsDomain') => {
125
+ const selectedDomainId = context?.fieldValues?.[domainFieldKey];
126
126
  const domainData = selectedDomainId ? getSmsDomainById(entity, selectedDomainId) : getSmsData(entity);
127
127
  if (!domainData) return [];
128
128
  return getOptionsFromContactInfo(domainData, 'gsm_sender_id', ['cdma_sender_id']);
129
129
  };
130
130
 
131
+ const getSmsSenderIdOptions = (entity, context = {}) => getSmsSenderIdOptionsForDomainField(entity, context, 'smsDomain');
132
+
131
133
  const getSmsSenderIdValue = (entity) => {
132
134
  const smsData = getSmsData(entity);
133
135
  return getValueFromContactInfo(smsData, 'gsm_sender_id', ['cdma_sender_id']);
@@ -359,6 +361,32 @@ export const getRcsAccountName = (entity) => {
359
361
  return rcsData.domainProperties?.domainName || rcsData.domainName || rcsData.label || '';
360
362
  };
361
363
 
364
+ // RCS SMS-fallback fields use SMS domain data and a pseudo-channel to include fallback fields in sender details.
365
+ export const RCS_SMS_FALLBACK_CHANNEL = 'RCS_SMS_FALLBACK';
366
+
367
+ /** Extract the SMS-fallback body text from an RCS templateData.smsFallBackContent record (CreativesContainer shape). */
368
+ export const getRcsSmsFallbackBodyText = (smsFallBackContent) => {
369
+ if (!smsFallBackContent || typeof smsFallBackContent !== 'object') return '';
370
+ return String(
371
+ smsFallBackContent.message
372
+ ?? smsFallBackContent.smsContent
373
+ ?? smsFallBackContent.smsTemplateContent
374
+ ?? smsFallBackContent.content
375
+ ?? '',
376
+ ).trim();
377
+ };
378
+
379
+ /** True when contentItems has an RCS item whose templateData.smsFallBackContent has real body text. */
380
+ export const hasRcsSmsFallbackContent = (contentItems = []) => {
381
+ const rcsItem = (contentItems || []).find((item) => (item?.channel || '')?.toUpperCase() === RCS);
382
+ return getRcsSmsFallbackBodyText(rcsItem?.templateData?.smsFallBackContent) !== '';
383
+ };
384
+
385
+ /** Default fallback SMS domain (first SMS domain option, same source as the SMS channel field) - for Reset. */
386
+ export const getRcsSmsFallbackDefaultDomain = (entity) => getSmsDomainOptions(entity)?.[0]?.value ?? getSmsDomainValue(entity) ?? '';
387
+
388
+ const getRcsSmsFallbackSenderIdOptions = (entity, context = {}) => getSmsSenderIdOptionsForDomainField(entity, context, 'rcsSmsFallbackDomain');
389
+
362
390
 
363
391
  // --- VIBER account (disabled) + sender ID ---
364
392
  // Aligned with adiona-ui/cap-campaigns-v2: domainId + sender (VIBER_SENDER_ID = 'sender')
@@ -524,7 +552,24 @@ export const DELIVERY_SETTINGS_FIELDS = [
524
552
  getValue: () => '',
525
553
  getOptions: () => [],
526
554
  },
527
- // RCS sender number: display-only in summary (DeliverySettingsSection); no select/reset in slidebox
555
+ // RCS sender number is display-only;
556
+ // SMS-fallback domain/sender ID render only when fallback content is configured.
557
+ {
558
+ channel: RCS_SMS_FALLBACK_CHANNEL,
559
+ fieldKey: 'rcsSmsFallbackDomain',
560
+ messageKey: 'fallbackSmsDomainLabel',
561
+ type: FIELD_TYPE.SELECT,
562
+ getValue: getSmsDomainValue,
563
+ getOptions: getSmsDomainOptions,
564
+ },
565
+ {
566
+ channel: RCS_SMS_FALLBACK_CHANNEL,
567
+ fieldKey: 'rcsSmsFallbackSenderId',
568
+ messageKey: 'fallbackSmsSenderIdLabel',
569
+ type: FIELD_TYPE.SELECT,
570
+ getValue: getSmsSenderIdValue,
571
+ getOptions: (entity, context) => getRcsSmsFallbackSenderIdOptions(entity, context),
572
+ },
528
573
  // VIBER - Viber account (disabled) + Sender ID (select with Reset) - per Figma
529
574
  {
530
575
  channel: 'VIBER',
@@ -594,6 +639,9 @@ export const parseEntityForDisplay = (entity, context = {}) => {
594
639
  whatsappAccountName: getWhatsappAccountName(entity, whatsappSourceAccountId),
595
640
  rcsSenderNumber: getRcsSenderNumberValue(entity),
596
641
  rcsAccountName: getRcsAccountName(entity),
642
+ // RCS SMS-fallback: sourced from the SMS domain data (same as the SMS channel's own fields).
643
+ rcsSmsFallbackDomain: getSmsDomainValue(entity),
644
+ rcsSmsFallbackSenderId: getSmsSenderIdValue(entity),
597
645
  senderNumber: getWhatsappSenderNumberValue(entity, { whatsappSourceAccountId }) || getRcsSenderNumberValue(entity),
598
646
  };
599
647
  };
@@ -625,10 +673,14 @@ export const buildChannelSettingFromFieldValues = (fieldValues = {}) => {
625
673
  senderMobNum: fieldValues.whatsappSenderNumber,
626
674
  };
627
675
  }
628
- if (fieldValues.rcsSenderNumber != null || fieldValues.rcsAccount != null) {
676
+ if (fieldValues.rcsSenderNumber != null || fieldValues.rcsAccount != null
677
+ || fieldValues.rcsSmsFallbackDomain != null || fieldValues.rcsSmsFallbackSenderId != null) {
629
678
  channelSetting.RCS = {
630
679
  senderMobNum: fieldValues.rcsSenderNumber,
631
680
  rcsSender: fieldValues.rcsSenderNumber,
681
+ // SMS-fallback sender ID/domain shown only when RCS SMS-fallback content is configured.
682
+ ...(fieldValues.rcsSmsFallbackDomain != null && { smsFallbackDomainId: fieldValues.rcsSmsFallbackDomain }),
683
+ ...(fieldValues.rcsSmsFallbackSenderId != null && { smsFallbackSenderId: fieldValues.rcsSmsFallbackSenderId }),
632
684
  };
633
685
  }
634
686
  // VIBER: aligned with adiona-ui (domainId + sender), campaigns-v2 (gsmSenderId/senderViber)
@@ -697,6 +749,8 @@ export const parseChannelSettingForDisplay = (channelSetting = {}) => {
697
749
  }
698
750
  if (channelSetting.RCS) {
699
751
  out.rcsSenderNumber = channelSetting.RCS.senderMobNum || channelSetting.RCS.rcsSender;
752
+ out.rcsSmsFallbackDomain = channelSetting.RCS.smsFallbackDomainId;
753
+ out.rcsSmsFallbackSenderId = channelSetting.RCS.smsFallbackSenderId;
700
754
  }
701
755
  // VIBER: read sender (adiona) or viberSenderId/gsmSenderId (campaigns compat)
702
756
  if (channelSetting.VIBER) {