@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
@@ -8,7 +8,7 @@
8
8
 
9
9
  import PropTypes from 'prop-types';
10
10
  import React, {
11
- useState, useEffect, useMemo, useRef,
11
+ useState, useEffect, useMemo, useRef, useCallback,
12
12
  } from 'react';
13
13
  import { FormattedMessage } from 'react-intl';
14
14
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
@@ -28,7 +28,8 @@ import CustomValuesEditor from './CustomValuesEditor';
28
28
  import SendTestMessage from './SendTestMessage';
29
29
  import PreviewSection from './PreviewSection';
30
30
 
31
- // Import constants
31
+ import * as Api from '../../services/api';
32
+ import { extractTemplateVariables } from '../../utils/templateVarUtils';
32
33
  import {
33
34
  CHANNELS,
34
35
  TEST,
@@ -70,10 +71,120 @@ import {
70
71
  IMAGE,
71
72
  VIDEO,
72
73
  URL,
74
+ PREVIEW_TAB_RCS,
75
+ PREVIEW_TAB_SMS_FALLBACK,
76
+ CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
77
+ RCS_TEST_META_CONTENT_TYPE_RICHCARD,
78
+ RCS_TEST_META_CARD_TYPE_STANDALONE,
79
+ RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
80
+ RCS_TEST_META_CARD_WIDTH_SMALL,
81
+ SMS_MUSTACHE_TAG_PATTERN,
73
82
  } from './constants';
74
-
75
- // Import utilities
76
83
  import { getCdnUrl } from '../../utils/cdnTransformation';
84
+ import {
85
+ normalizePreviewApiPayload,
86
+ extractPreviewFromLiquidResponse,
87
+ getSmsFallbackTextForTagExtraction,
88
+ } from './previewApiUtils';
89
+
90
+ /**
91
+ * Drop empty GSM rows. RCS/DLT responses often set gsm_sender_id equal to domainName — keep those rows
92
+ * for RCS defaults (ModifyDeliverySettings uses the same rule for the sender dropdown).
93
+ */
94
+ const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEchoFilter = false } = {}) => {
95
+ const normalizedDomainName =
96
+ domain?.domainName != null ? String(domain.domainName).trim().toLowerCase() : '';
97
+ return (gsmSenders || []).filter((gsmSenderRow) => {
98
+ const rawValue = gsmSenderRow?.value;
99
+ if (rawValue == null) return false;
100
+ const trimmedSenderValue = String(rawValue).trim();
101
+ if (!trimmedSenderValue) return false;
102
+ const senderMatchesDomainLabel =
103
+ normalizedDomainName && trimmedSenderValue.toLowerCase() === normalizedDomainName;
104
+ if (!skipDomainNameEchoFilter && senderMatchesDomainLabel) return false;
105
+ return true;
106
+ });
107
+ };
108
+
109
+ /** Preview payload from Redux may be an Immutable Map — normalize for React state. */
110
+ const toPlainPreviewData = (data) => {
111
+ if (data == null) return null;
112
+ const plain = typeof data.toJS === 'function' ? data.toJS() : data;
113
+ return normalizePreviewApiPayload(plain);
114
+ };
115
+
116
+ /**
117
+ * Merge existing customValues with tag keys from categorized groups.
118
+ * Each group is { required, optional } (arrays of tag objects with fullPath).
119
+ * Preserves existing values; adds '' for any tag key not yet present.
120
+ * Reusable for RCS+fallback and any flow that needs to ensure customValues has keys for tags.
121
+ */
122
+ const mergeCustomValuesWithTagKeys = (prev, ...categorizedGroups) => {
123
+ const next = { ...(prev || {}) };
124
+ categorizedGroups.forEach((group) => {
125
+ [...(group.required || []), ...(group.optional || [])].forEach((tag) => {
126
+ const key = tag?.fullPath;
127
+ if (key && next[key] === undefined) next[key] = '';
128
+ });
129
+ });
130
+ return next;
131
+ };
132
+
133
+ /** True when `body` contains `{{name}}` mustache tokens (user-fillable personalization tags).
134
+ * DLT `{#name#}` slots are pre-bound template variables and are intentionally excluded. */
135
+ const smsTemplateHasMustacheTags = (body) =>
136
+ typeof body === 'string' && SMS_MUSTACHE_TAG_PATTERN.test(body);
137
+
138
+ /**
139
+ * Build tag rows from `{{…}}` mustache tokens only — DLT `{#…#}` slots are excluded because
140
+ * they are pre-bound template variables, not user-fillable personalization tags.
141
+ * Passing a mustache-only captureRegex to extractTemplateVariables skips the DLT branch.
142
+ * A non-global regex is used so ensureGlobalRegexForExecLoop creates a fresh instance on each call.
143
+ */
144
+ const buildSyntheticSmsMustacheTags = (body = '') => {
145
+ if (!body || typeof body !== 'string') return [];
146
+ return extractTemplateVariables(body, /\{\{([^}]+)\}\}/).map((name) => ({
147
+ name,
148
+ metaData: { userDriven: false },
149
+ children: [],
150
+ }));
151
+ };
152
+
153
+ /** RCS createMessageMeta: media shape (mediaUrl, thumbnailUrl, height string). */
154
+ const normalizeRcsTestCardMedia = (media) => {
155
+ if (!media || typeof media !== 'object') return undefined;
156
+ const mediaUrl =
157
+ media.mediaUrl != null && String(media.mediaUrl).trim() !== ''
158
+ ? String(media.mediaUrl)
159
+ : media.url != null && String(media.url).trim() !== ''
160
+ ? String(media.url)
161
+ : '';
162
+ const thumbnailUrl = media.thumbnailUrl != null ? String(media.thumbnailUrl) : '';
163
+ const height = media.height != null ? String(media.height) : undefined;
164
+ const out = { mediaUrl, thumbnailUrl };
165
+ if (height) out.height = height;
166
+ return out;
167
+ };
168
+
169
+ /** RCS createMessageMeta: suggestion shape (index, type, text, phoneNumber, url, postback). */
170
+ const mapRcsSuggestionForTestMeta = (suggestionRow, index) => ({
171
+ index,
172
+ type: suggestionRow?.type ?? '',
173
+ text: suggestionRow?.text != null ? String(suggestionRow.text) : '',
174
+ phoneNumber:
175
+ suggestionRow?.phoneNumber != null
176
+ ? String(suggestionRow.phoneNumber)
177
+ : suggestionRow?.phone_number != null
178
+ ? String(suggestionRow.phone_number)
179
+ : '',
180
+ url: suggestionRow?.url !== undefined ? suggestionRow.url : null,
181
+ postback:
182
+ suggestionRow?.postback != null
183
+ ? String(suggestionRow.postback)
184
+ : suggestionRow?.text != null
185
+ ? String(suggestionRow.text)
186
+ : '',
187
+ });
77
188
 
78
189
  /**
79
190
  * Preview Component Factory - REMOVED IN PHASE 5
@@ -85,7 +196,7 @@ import { getCdnUrl } from '../../utils/cdnTransformation';
85
196
  */
86
197
  const CommonTestAndPreview = (props) => {
87
198
  const {
88
- intl: { formatMessage },
199
+ intl: { formatMessage, locale: userLocale = 'en' },
89
200
  show,
90
201
  onClose,
91
202
  channel, // The channel: 'EMAIL', 'SMS', 'RCS', etc.
@@ -121,19 +232,29 @@ const CommonTestAndPreview = (props) => {
121
232
  ...additionalProps
122
233
  } = props;
123
234
 
235
+ const smsFallbackContent = additionalProps?.smsFallbackContent;
236
+ const smsFallbackTextForTagExtraction = useMemo(
237
+ () => getSmsFallbackTextForTagExtraction(smsFallbackContent),
238
+ [smsFallbackContent],
239
+ );
124
240
  // ============================================
125
241
  // STATE MANAGEMENT
126
242
  // ============================================
127
243
  const [selectedCustomer, setSelectedCustomer] = useState(null);
128
244
  const [requiredTags, setRequiredTags] = useState([]);
129
245
  const [optionalTags, setOptionalTags] = useState([]);
246
+ const [smsFallbackExtractedTags, setSmsFallbackExtractedTags] = useState([]);
247
+ const [smsFallbackRequiredTags, setSmsFallbackRequiredTags] = useState([]);
248
+ const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
249
+ const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
130
250
  const [customValues, setCustomValues] = useState({});
131
251
  const [showJSON, setShowJSON] = useState(false);
132
252
  const [tagsExtracted, setTagsExtracted] = useState(false);
133
- // Initialize device based on channel: SMS uses Android/iOS, others use Desktop/Mobile
134
- // Initialize device based on channel: SMS, WhatsApp, RCS, InApp, MobilePush, and Viber use Android/iOS, others use Desktop/Mobile
135
- const initialDevice = (channel === CHANNELS.SMS || channel === CHANNELS.WHATSAPP || channel === CHANNELS.RCS || channel === CHANNELS.INAPP || channel === CHANNELS.MOBILEPUSH || channel === CHANNELS.VIBER) ? ANDROID : DESKTOP;
253
+
254
+ const initialDevice = CHANNELS_USING_ANDROID_PREVIEW_DEVICE.includes(channel) ? ANDROID : DESKTOP;
136
255
  const [previewDevice, setPreviewDevice] = useState(initialDevice);
256
+ const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
257
+ const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
137
258
  // Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
138
259
  const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
139
260
  const [previewDataHtml, setPreviewDataHtml] = useState(() => {
@@ -166,15 +287,22 @@ const CommonTestAndPreview = (props) => {
166
287
  [CHANNELS.WHATSAPP]: {
167
288
  domainId: null, senderMobNum: '', sourceAccountIdentifier: '',
168
289
  },
290
+ [CHANNELS.RCS]: {
291
+ domainId: null,
292
+ domainGatewayMapId: null,
293
+ gsmSenderId: '',
294
+ smsFallbackDomainId: null,
295
+ cdmaSenderId: '', // gsmSenderId = RCS sender (domainId|senderId), cdmaSenderId = SMS fallback
296
+ },
169
297
  });
170
298
 
171
- const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP];
299
+ const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
172
300
  const formDataForSendTest = formData ?? (content && typeof content === 'object' && !Array.isArray(content) ? content : formData);
173
301
  const smsTemplateConfigs = formDataForSendTest?.templateConfigs || {};
174
302
  const smsTraiDltEnabled = !!smsTemplateConfigs?.traiDltEnabled;
175
303
  const registeredSenderIds = smsTemplateConfigs?.registeredSenderIds || [];
176
304
 
177
- // Fetch sender details and WeCRM accounts when Test & Preview opens for SMS/Email/WhatsApp
305
+ // Fetch sender details and WeCRM accounts when Test & Preview opens (SMS, Email, WhatsApp, RCS — same process)
178
306
  useEffect(() => {
179
307
  if (!show || !channel) {
180
308
  return;
@@ -183,6 +311,10 @@ const CommonTestAndPreview = (props) => {
183
311
  if (actions.getSenderDetailsRequested) {
184
312
  actions.getSenderDetailsRequested({ channel, orgUnitId: orgUnitId ?? -1 });
185
313
  }
314
+ // SMS domains/senders are needed for RCS delivery UI (fallback row + slidebox) whenever RCS is open — not only when fallback body exists.
315
+ if (channel === CHANNELS.RCS && actions.getSenderDetailsRequested) {
316
+ actions.getSenderDetailsRequested({ channel: CHANNELS.SMS, orgUnitId: orgUnitId ?? -1 });
317
+ }
186
318
  if (channel === CHANNELS.WHATSAPP && actions.getWeCrmAccountsRequested) {
187
319
  actions.getWeCrmAccountsRequested({ sourceName: CHANNELS.WHATSAPP });
188
320
  }
@@ -194,20 +326,84 @@ const CommonTestAndPreview = (props) => {
194
326
  // Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
195
327
  useEffect(() => {
196
328
  if (!channel || !channelsWithDeliverySettings.includes(channel)) return;
329
+
330
+ if (channel === CHANNELS.RCS) {
331
+ const rcsDomainRows = senderDetailsByChannel?.[CHANNELS.RCS] || [];
332
+ const smsFallbackDomainRows = senderDetailsByChannel?.[CHANNELS.SMS] || [];
333
+ if (!rcsDomainRows.length) return;
334
+
335
+ const currentRcsDeliverySettings = testPreviewDeliverySettings?.[CHANNELS.RCS] || {};
336
+ const isRcsGsmSenderUnset = !currentRcsDeliverySettings?.gsmSenderId;
337
+ const isSmsFallbackSenderUnset = !currentRcsDeliverySettings?.cdmaSenderId;
338
+ const isRcsDeliveryFullyUnset = isRcsGsmSenderUnset && isSmsFallbackSenderUnset;
339
+ const shouldOnlyFillSmsFallbackSender =
340
+ !isRcsGsmSenderUnset && isSmsFallbackSenderUnset && smsFallbackDomainRows.length > 0;
341
+
342
+ if (!isRcsDeliveryFullyUnset && !shouldOnlyFillSmsFallbackSender) return;
343
+
344
+ const firstRcsDomain = rcsDomainRows[0];
345
+ const firstSmsFallbackDomain = smsFallbackDomainRows[0];
346
+ const usableRcsGsmSenders = filterUsableGsmSendersForDomain(
347
+ firstRcsDomain,
348
+ firstRcsDomain?.gsmSenders,
349
+ { skipDomainNameEchoFilter: true },
350
+ );
351
+ const usableSmsFallbackGsmSenders = firstSmsFallbackDomain
352
+ ? filterUsableGsmSendersForDomain(firstSmsFallbackDomain, firstSmsFallbackDomain?.gsmSenders)
353
+ : [];
354
+ const defaultRcsGsmSender = usableRcsGsmSenders[0];
355
+ const defaultSmsFallbackGsmSender = usableSmsFallbackGsmSenders[0];
356
+ const rcsSenderCompositeValue =
357
+ firstRcsDomain?.domainId != null && defaultRcsGsmSender?.value != null
358
+ ? `${firstRcsDomain.domainId}|${defaultRcsGsmSender.value}`
359
+ : (defaultRcsGsmSender?.value || '');
360
+ const smsFallbackSenderCompositeValue =
361
+ firstSmsFallbackDomain?.domainId != null && defaultSmsFallbackGsmSender?.value != null
362
+ ? `${firstSmsFallbackDomain.domainId}|${defaultSmsFallbackGsmSender.value}`
363
+ : (defaultSmsFallbackGsmSender?.value || '');
364
+
365
+ setTestPreviewDeliverySettings((prev) => {
366
+ const previousRcsSettings = prev?.[CHANNELS.RCS] || {};
367
+ if (shouldOnlyFillSmsFallbackSender) {
368
+ if (!smsFallbackSenderCompositeValue) return prev;
369
+ return {
370
+ ...prev,
371
+ [CHANNELS.RCS]: {
372
+ ...previousRcsSettings,
373
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
374
+ cdmaSenderId: smsFallbackSenderCompositeValue,
375
+ },
376
+ };
377
+ }
378
+ return {
379
+ ...prev,
380
+ [CHANNELS.RCS]: {
381
+ domainId: firstRcsDomain?.domainId ?? null,
382
+ domainGatewayMapId: firstRcsDomain?.dgmId ?? null,
383
+ gsmSenderId: rcsSenderCompositeValue,
384
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
385
+ cdmaSenderId: smsFallbackSenderCompositeValue,
386
+ },
387
+ };
388
+ });
389
+ return;
390
+ }
391
+
197
392
  const domains = senderDetailsByChannel[channel];
198
393
  if (!domains || domains.length === 0) return;
199
394
  const {
200
- domainId = '', gsmSenderId = '', senderEmail = '', senderMobNum = '',
395
+ domainId = '', gsmSenderId = '', cdmaSenderId = '', senderEmail = '', senderMobNum = '',
201
396
  } = testPreviewDeliverySettings[channel] || {};
202
- const isEmptySelection = !domainId && !gsmSenderId && !senderEmail && !senderMobNum;
397
+ const isEmptySelection = !domainId && !gsmSenderId && !cdmaSenderId && !senderEmail && !senderMobNum;
203
398
  if (!isEmptySelection) return;
204
399
 
205
400
  const whatsappAccountFromForm = channel === CHANNELS.WHATSAPP ? formData?.accountName : undefined;
206
401
  const matchedWhatsappAccount = whatsappAccountFromForm
207
402
  ? (wecrmAccounts || []).find((account) => account?.name === whatsappAccountFromForm)
208
403
  : null;
209
- const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled
210
- ? domains.filter((domain) => (domain.gsmSenders || []).some((gsm) => registeredSenderIds.includes(gsm.value)))
404
+ const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled && registeredSenderIds?.length
405
+ ? domains.filter((domain) => (domain?.gsmSenders || []).some((gsm) =>
406
+ registeredSenderIds?.includes(gsm?.value)))
211
407
  : domains;
212
408
  const [defaultDomain] = domains;
213
409
  const [firstSmsDomain] = smsDomains;
@@ -222,8 +418,8 @@ const CommonTestAndPreview = (props) => {
222
418
  const next = { ...prev };
223
419
  if (channel === CHANNELS.SMS) {
224
420
  const smsGsmSenders = smsTraiDltEnabled
225
- ? (firstDomain.gsmSenders || []).filter((gsm) => registeredSenderIds.includes(gsm.value))
226
- : firstDomain.gsmSenders;
421
+ ? (firstDomain?.gsmSenders || []).filter((gsm) => registeredSenderIds?.includes(gsm?.value))
422
+ : firstDomain?.gsmSenders;
227
423
  next[channel] = {
228
424
  domainId: firstDomain.domainId,
229
425
  domainGatewayMapId: firstDomain.dgmId,
@@ -256,10 +452,29 @@ const CommonTestAndPreview = (props) => {
256
452
  // MEMOIZED VALUES
257
453
  // ============================================
258
454
 
455
+ const allTags = useMemo(
456
+ () => [...requiredTags, ...optionalTags, ...smsFallbackRequiredTags, ...smsFallbackOptionalTags],
457
+ [requiredTags, optionalTags, smsFallbackRequiredTags, smsFallbackOptionalTags]
458
+ );
459
+
460
+ const allRequiredTags = useMemo(
461
+ () => [...requiredTags, ...smsFallbackRequiredTags],
462
+ [requiredTags, smsFallbackRequiredTags]
463
+ );
464
+
465
+ const buildEmptyValues = useCallback(
466
+ () => allTags.reduce((acc, tag) => {
467
+ const key = tag?.fullPath;
468
+ if (key) acc[key] = '';
469
+ return acc;
470
+ }, {}),
471
+ [allTags]
472
+ );
473
+
259
474
  // Check if update preview button should be disabled
260
475
  const isUpdatePreviewDisabled = useMemo(() => (
261
- requiredTags.some((tag) => !customValues[tag.fullPath])
262
- ), [requiredTags, customValues]);
476
+ allRequiredTags.some((tag) => !customValues[tag.fullPath])
477
+ ), [allRequiredTags, customValues]);
263
478
 
264
479
  // Get current content based on channel and editor type
265
480
  const getCurrentContent = useMemo(() => {
@@ -303,6 +518,13 @@ const CommonTestAndPreview = (props) => {
303
518
  return currentTabData.base['sms-editor'];
304
519
  }
305
520
  }
521
+ // DLT / Test & Preview shape: { templateConfigs: { template, templateId, ... } }
522
+ if (formData.templateConfigs?.template) {
523
+ const smsDltTemplateValue = formData.templateConfigs.template;
524
+ if (typeof smsDltTemplateValue === 'string') return smsDltTemplateValue;
525
+ if (Array.isArray(smsDltTemplateValue)) return smsDltTemplateValue.join('');
526
+ return '';
527
+ }
306
528
  }
307
529
 
308
530
  // SMS channel fallback - if formData is not available, use content directly
@@ -348,7 +570,70 @@ const CommonTestAndPreview = (props) => {
348
570
  return content || '';
349
571
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
350
572
 
351
- // Build test entities tree data
573
+ const leftPanelExtractedTags = useMemo(() => {
574
+ if (channel === CHANNELS.SMS) {
575
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
576
+ if (!smsTemplateHasMustacheTags(smsEditorBody)) return [];
577
+ const extractTagsFromApi = extractedTags ?? [];
578
+ if (extractTagsFromApi.length > 0) return extractTagsFromApi;
579
+ return buildSyntheticSmsMustacheTags(smsEditorBody);
580
+ }
581
+ const hasFallbackSmsBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
582
+ if (channel === CHANNELS.RCS && hasFallbackSmsBody) {
583
+ const rcsPrimaryTags = extractedTags ?? [];
584
+ const fallbackSmsTextForTags = smsFallbackTextForTagExtraction ?? '';
585
+ const fallbackSmsTagRows = smsTemplateHasMustacheTags(fallbackSmsTextForTags)
586
+ ? (smsFallbackExtractedTags?.length > 0
587
+ ? smsFallbackExtractedTags
588
+ : buildSyntheticSmsMustacheTags(fallbackSmsTextForTags))
589
+ : [];
590
+ const mergedRcsAndFallbackTags = [...rcsPrimaryTags, ...fallbackSmsTagRows];
591
+ if (mergedRcsAndFallbackTags.length > 0) return mergedRcsAndFallbackTags;
592
+ return buildSyntheticSmsMustacheTags(fallbackSmsTextForTags);
593
+ }
594
+ return extractedTags ?? [];
595
+ }, [
596
+ channel,
597
+ extractedTags,
598
+ getCurrentContent,
599
+ smsFallbackContent,
600
+ smsFallbackExtractedTags,
601
+ smsFallbackTextForTagExtraction,
602
+ ]);
603
+
604
+ const isRcsSmsFallbackPreviewEnabled =
605
+ channel === CHANNELS.RCS
606
+ && !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
607
+ // Only treat as SMS when user is on the Fallback SMS tab — not whenever fallback exists (RCS tab needs RCS preview API).
608
+ const isSmsFallbackTabActive = isRcsSmsFallbackPreviewEnabled && activePreviewTab === PREVIEW_TAB_SMS_FALLBACK;
609
+ const activeChannelForActions = isSmsFallbackTabActive ? CHANNELS.SMS : channel;
610
+ // VarSegment slot values live in rcsSmsFallbackVarMapped; raw templateContent alone is stale for /preview Body.
611
+ const resolvedSmsFallbackBodyForPreviewTab =
612
+ smsFallbackTextForTagExtraction
613
+ || smsFallbackContent?.templateContent
614
+ || smsFallbackContent?.content
615
+ || '';
616
+ const activeContentForActions = isSmsFallbackTabActive
617
+ ? resolvedSmsFallbackBodyForPreviewTab
618
+ : getCurrentContent;
619
+
620
+ /**
621
+ * SMS fallback pane must show /preview API result when user updated preview on that tab (plain text).
622
+ * Skip when resolvedBody is RCS-shaped (e.g. user last previewed on RCS tab).
623
+ */
624
+ // smsFallbackPreviewText is the single source of truth for the resolved SMS fallback preview.
625
+ // It is set only by syncSmsFallbackPreview (called from handleUpdatePreview and the
626
+ // prefilled-values effect) and reset to undefined on discard / slidebox close.
627
+ // Using previewDataHtml as a fallback is unsafe because that state is shared with the primary
628
+ // RCS preview and can contain stale SMS or RCS content.
629
+ const smsFallbackResolvedText = useMemo(() => {
630
+ const hasFallbackBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
631
+ if (channel !== CHANNELS.RCS || !hasFallbackBody) return undefined;
632
+ if (smsFallbackPreviewText != null) return smsFallbackPreviewText;
633
+ return undefined;
634
+ }, [channel, smsFallbackContent, smsFallbackPreviewText]);
635
+
636
+ // Build test entities tree data from testCustomers prop
352
637
  const testEntitiesTreeData = useMemo(() => {
353
638
  const groupsNode = {
354
639
  title: 'Groups',
@@ -460,6 +745,37 @@ const CommonTestAndPreview = (props) => {
460
745
  }
461
746
  };
462
747
 
748
+ /**
749
+ * When RCS has SMS fallback, refresh fallback preview text via the same Liquid /preview API
750
+ * (separate call with SMS channel + fallback template body). Used after primary preview updates.
751
+ */
752
+ const syncSmsFallbackPreview = async (customValuesForResolve, selectedCustomerObj) => {
753
+ const fallbackBodyForLiquidPreview =
754
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
755
+ || smsFallbackContent?.templateContent
756
+ || smsFallbackContent?.content
757
+ || '';
758
+ if (channel !== CHANNELS.RCS || !String(fallbackBodyForLiquidPreview).trim()) return;
759
+ try {
760
+ const smsFallbackPayload = preparePreviewPayload(
761
+ CHANNELS.SMS,
762
+ formData || {},
763
+ fallbackBodyForLiquidPreview,
764
+ customValuesForResolve,
765
+ selectedCustomerObj
766
+ );
767
+ const fallbackResponse = await Api.updateEmailPreview(smsFallbackPayload);
768
+ const fallbackPreview = extractPreviewFromLiquidResponse(fallbackResponse);
769
+ setSmsFallbackPreviewText(
770
+ typeof fallbackPreview?.resolvedBody === 'string'
771
+ ? fallbackPreview.resolvedBody
772
+ : undefined
773
+ );
774
+ } catch (e) {
775
+ /* keep existing smsFallbackPreviewText on failure */
776
+ }
777
+ };
778
+
463
779
  /**
464
780
  * Prepare payload for tag extraction based on channel
465
781
  */
@@ -554,7 +870,7 @@ const CommonTestAndPreview = (props) => {
554
870
  } = carousel || {};
555
871
  const buttonData = buttons.map((button, index) => {
556
872
  const {
557
- type, text, phone_number, urlType, url,
873
+ type, text, phone_number: phoneNumber, urlType, url,
558
874
  } = button || {};
559
875
  const buttonObj = {
560
876
  type,
@@ -562,7 +878,7 @@ const CommonTestAndPreview = (props) => {
562
878
  index,
563
879
  };
564
880
  if (type === PHONE_NUMBER) {
565
- buttonObj.phoneNumber = phone_number;
881
+ buttonObj.phoneNumber = phoneNumber;
566
882
  }
567
883
  if (type === URL) {
568
884
  buttonObj.url = url;
@@ -591,7 +907,131 @@ const CommonTestAndPreview = (props) => {
591
907
  };
592
908
  });
593
909
 
594
- const prepareTestMessagePayload = (channelType, formDataObj, contentStr, customValuesObj, recipientDetails, previewDataObj, deliverySettingsOverride) => {
910
+ /**
911
+ * Build createMessageMeta payload for RCS (test message).
912
+ * rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
913
+ * Then rcsDeliverySettings, executionParams, clientName last.
914
+ */
915
+ const buildRcsTestMessagePayload = (formDataObj, _contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra = {}) => {
916
+ const plainCustom =
917
+ customValuesObj != null && typeof customValuesObj.toJS === 'function'
918
+ ? customValuesObj.toJS()
919
+ : (customValuesObj || {});
920
+ const userVarMap = Object.fromEntries(
921
+ Object.entries(plainCustom).filter(([, v]) => v != null && String(v).trim() !== '')
922
+ );
923
+ const rcsData = formDataObj?.versions?.base?.content?.RCS ?? formDataObj?.content?.RCS ?? {};
924
+ const rcsContent = rcsData?.rcsContent || {};
925
+ const smsFallback = rcsData?.smsFallBackContent || {};
926
+ let cardContentList = [];
927
+ if (Array.isArray(rcsContent?.cardContent)) {
928
+ cardContentList = rcsContent.cardContent;
929
+ } else if (rcsContent?.cardContent) {
930
+ cardContentList = [rcsContent.cardContent];
931
+ }
932
+ // Merge test customValues into cardVarMapped (template snapshot + user-entered RCS + fallback SMS tags).
933
+ const cardContent = cardContentList.map((rcsCard) => {
934
+ const baseMap = rcsCard.cardVarMapped || {};
935
+ const mergedCardVarMapped =
936
+ Object.keys(userVarMap).length > 0 ? { ...baseMap, ...userVarMap } : baseMap;
937
+ const mediaNorm = rcsCard?.media ? normalizeRcsTestCardMedia(rcsCard.media) : undefined;
938
+ const suggestionsRaw = Array.isArray(rcsCard?.suggestions) ? rcsCard.suggestions : [];
939
+ const suggestionsMapped = suggestionsRaw.map((suggestionItem, i) =>
940
+ mapRcsSuggestionForTestMeta(suggestionItem, i));
941
+ return {
942
+ title: rcsCard?.title ?? '',
943
+ description: rcsCard?.description ?? '',
944
+ mediaType: rcsCard?.mediaType ?? MEDIA_TYPE_TEXT,
945
+ ...(mediaNorm && { media: mediaNorm }),
946
+ ...(Object.keys(mergedCardVarMapped).length > 0 && { cardVarMapped: mergedCardVarMapped }),
947
+ ...(suggestionsMapped.length > 0 && { suggestions: suggestionsMapped }),
948
+ };
949
+ });
950
+ // Prefer parent `smsFallbackContent` snapshot (rcsExtra) — includes VarSegment-resolved body via
951
+ // getSmsFallbackTextForTagExtraction. Nested formData.smsFallBackContent is often stale vs live editor.
952
+ const rcsExtraFallbackTemplate = rcsExtra?.smsFallbackTemplateContent;
953
+ const hasResolvedFallbackBodyFromRcsExtra =
954
+ rcsExtraFallbackTemplate != null
955
+ && String(rcsExtraFallbackTemplate).trim() !== '';
956
+ const smsMessageRaw = hasResolvedFallbackBodyFromRcsExtra
957
+ ? String(rcsExtraFallbackTemplate)
958
+ : (smsFallback?.smsContent ?? smsFallback?.message ?? '');
959
+ const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
960
+ ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
961
+ : deliverySettingsOverride?.cdmaSenderId;
962
+ const deliveryFallbackSmsId =
963
+ typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
964
+ const creativeFallbackSmsId =
965
+ smsFallback?.senderId != null ? String(smsFallback.senderId).trim() : '';
966
+ const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
967
+
968
+ const smsFallBackContent =
969
+ smsMessageRaw.trim() !== ''
970
+ ? { message: smsMessageRaw }
971
+ : undefined;
972
+
973
+ // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
974
+ const accountIdForMeta =
975
+ rcsContent?.accountId != null && String(rcsContent.accountId).trim() !== ''
976
+ ? String(rcsContent.accountId)
977
+ : undefined;
978
+
979
+ const rcsRichCardContent = {
980
+ contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
981
+ cardType: rcsContent?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
982
+ cardSettings: rcsContent?.cardSettings ?? {
983
+ cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
984
+ cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
985
+ },
986
+ ...(cardContent.length > 0 && { cardContent }),
987
+ };
988
+
989
+ const rcsMessageContent = {
990
+ channel: CHANNELS.RCS,
991
+ ...(accountIdForMeta && { accountId: accountIdForMeta }),
992
+ rcsRichCardContent,
993
+ ...(smsFallBackContent && { smsFallBackContent }),
994
+ };
995
+ const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
996
+ const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
997
+ const rcsDeliverySettings = {
998
+ channelSettings: {
999
+ channel: CHANNELS.RCS,
1000
+ rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
1001
+ domainId:
1002
+ rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
1003
+ ? Number(rcsDomainId)
1004
+ : (deliverySettingsOverride?.domainId ?? 0),
1005
+ fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
1006
+ },
1007
+ additionalSettings: {
1008
+ useTinyUrl: false,
1009
+ encryptUrl: false,
1010
+ linkTrackingEnabled: false,
1011
+ bypassControlUser: false,
1012
+ userSubscriptionDisabled: false,
1013
+ },
1014
+ };
1015
+ const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
1016
+ return {
1017
+ ...restBase,
1018
+ rcsMessageContent,
1019
+ rcsDeliverySettings,
1020
+ executionParams: {},
1021
+ clientName: baseClientName,
1022
+ };
1023
+ };
1024
+
1025
+ const prepareTestMessagePayload = (
1026
+ channelType,
1027
+ formDataObj,
1028
+ contentStr,
1029
+ customValuesObj,
1030
+ recipientDetails,
1031
+ previewDataObj,
1032
+ deliverySettingsOverride,
1033
+ rcsExtra = {},
1034
+ ) => {
595
1035
  // Base payload structure common to all channels
596
1036
  const basePayload = {
597
1037
  ouId: -1,
@@ -672,12 +1112,12 @@ const CommonTestAndPreview = (props) => {
672
1112
  },
673
1113
  smsDeliverySettings: {
674
1114
  channelSettings: {
1115
+ channel: CHANNELS.SMS,
675
1116
  gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
676
1117
  domainId: deliverySettingsOverride?.domainId ?? null,
677
1118
  domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
678
1119
  targetNdnc: false,
679
1120
  cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
680
- channel: CHANNELS.SMS,
681
1121
  },
682
1122
  additionalSettings: {
683
1123
  useTinyUrl: false,
@@ -821,7 +1261,7 @@ const CommonTestAndPreview = (props) => {
821
1261
  return {
822
1262
  ...basePayload,
823
1263
  whatsappMessageContent: {
824
- messageBody: templateEditorValue || '',
1264
+ messageBody: resolvedMessageBody || templateEditorValue || '',
825
1265
  accountId: formDataObj?.accountId || '',
826
1266
  sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
827
1267
  accountName: formDataObj?.accountName || '',
@@ -846,16 +1286,7 @@ const CommonTestAndPreview = (props) => {
846
1286
  }
847
1287
 
848
1288
  case CHANNELS.RCS:
849
- return {
850
- ...basePayload,
851
- rcsMessageContent: {
852
- channel: CHANNELS.RCS,
853
- messageBody: contentStr,
854
- rcsType: additionalProps?.rcsType,
855
- rcsImageSrc: formDataObj?.rcsImageSrc,
856
- rcsSuggestions: formDataObj?.rcsSuggestions,
857
- },
858
- };
1289
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
859
1290
 
860
1291
  case CHANNELS.INAPP: {
861
1292
  // InApp payload structure similar to MobilePush
@@ -1957,6 +2388,10 @@ const CommonTestAndPreview = (props) => {
1957
2388
  formatMessage,
1958
2389
  lastModified: formData?.lastModified,
1959
2390
  updatedByName: formData?.updatedByName,
2391
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2392
+ smsFallbackResolvedText,
2393
+ activePreviewTab,
2394
+ onPreviewTabChange: setActivePreviewTab,
1960
2395
  };
1961
2396
  };
1962
2397
 
@@ -1996,7 +2431,12 @@ const CommonTestAndPreview = (props) => {
1996
2431
  }, [show, beeInstance, currentTab, channel]);
1997
2432
 
1998
2433
  /**
1999
- * Initial data load when slidebox opens
2434
+ * Initial data load when slidebox opens.
2435
+ * EXTRACT TAGS CALL SITES (on open/edit RCS):
2436
+ * 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
2437
+ * 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
2438
+ * 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
2439
+ * The "Process extracted tags" effect only processes API results and must not call extract again.
2000
2440
  */
2001
2441
  useEffect(() => {
2002
2442
  if (show) {
@@ -2081,7 +2521,61 @@ const CommonTestAndPreview = (props) => {
2081
2521
  actions.getTestGroupsRequested();
2082
2522
  }
2083
2523
  }
2084
- }, [show, beeInstance, currentTab, channel]);
2524
+ // getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
2525
+ }, [show, beeInstance, currentTab, channel, getCurrentContent]);
2526
+
2527
+ /**
2528
+ * RCS with SMS fallback: extract tags for fallback SMS content as well
2529
+ * (so we can show a separate "Fallback SMS tags" section in left panel).
2530
+ */
2531
+ useEffect(() => {
2532
+ let cancelled = false;
2533
+
2534
+ if (!show || channel !== CHANNELS.RCS) {
2535
+ return () => {
2536
+ cancelled = true;
2537
+ };
2538
+ }
2539
+
2540
+ if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
2541
+ setSmsFallbackExtractedTags([]);
2542
+ setSmsFallbackRequiredTags([]);
2543
+ setSmsFallbackOptionalTags([]);
2544
+ return () => {
2545
+ cancelled = true;
2546
+ };
2547
+ }
2548
+
2549
+ setIsExtractingSmsFallbackTags(true);
2550
+ (async () => {
2551
+ try {
2552
+ const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
2553
+ const payload = {
2554
+ messageTitle: '',
2555
+ messageBody: fallbackBodyForExtractApi || '',
2556
+ };
2557
+ const response = await Api.extractTagsWithMetaData(payload); //not using saga action here because we dont store fallbacksms related data in store but only in useState since this is only used in RCS SMS fallback
2558
+ let smsFallbackTagTree = response?.data ?? [];
2559
+ if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
2560
+ if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
2561
+ smsFallbackTagTree = [];
2562
+ } else if (smsFallbackTagTree.length === 0) {
2563
+ smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
2564
+ }
2565
+ if (cancelled) return;
2566
+ setSmsFallbackExtractedTags(smsFallbackTagTree);
2567
+ } catch (e) {
2568
+ if (cancelled) return;
2569
+ setSmsFallbackExtractedTags([]);
2570
+ } finally {
2571
+ if (!cancelled) setIsExtractingSmsFallbackTags(false);
2572
+ }
2573
+ })();
2574
+
2575
+ return () => {
2576
+ cancelled = true;
2577
+ };
2578
+ }, [show, channel, smsFallbackContent]);
2085
2579
 
2086
2580
  /**
2087
2581
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -2133,15 +2627,22 @@ const CommonTestAndPreview = (props) => {
2133
2627
  setSelectedCustomer(null);
2134
2628
  setRequiredTags([]);
2135
2629
  setOptionalTags([]);
2630
+ setSmsFallbackExtractedTags([]);
2631
+ setSmsFallbackRequiredTags([]);
2632
+ setSmsFallbackOptionalTags([]);
2633
+ setIsExtractingSmsFallbackTags(false);
2136
2634
  setCustomValues({});
2137
2635
  setShowJSON(false);
2138
2636
  setTagsExtracted(false);
2139
2637
  setPreviewDevice(DESKTOP);
2638
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2639
+ setSmsFallbackPreviewText(undefined);
2140
2640
  setSelectedTestEntities([]);
2141
2641
  actions.clearPrefilledValues();
2142
2642
  } else {
2143
2643
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
2144
2644
  setPreviewDevice(initialDevice);
2645
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2145
2646
  }
2146
2647
  }, [show, initialDevice]);
2147
2648
 
@@ -2150,79 +2651,10 @@ const CommonTestAndPreview = (props) => {
2150
2651
  */
2151
2652
  useEffect(() => {
2152
2653
  if (previewData) {
2153
- setPreviewDataHtml(previewData);
2654
+ setPreviewDataHtml(toPlainPreviewData(previewData));
2154
2655
  }
2155
2656
  }, [previewData]);
2156
2657
 
2157
- /**
2158
- * Process extracted tags and categorize them
2159
- */
2160
- useEffect(() => {
2161
- // Categorize tags into required and optional
2162
- const required = [];
2163
- const optional = [];
2164
- let hasPersonalizationTags = false;
2165
-
2166
- if (extractedTags?.length > 0) {
2167
- const processTag = (tag, parentPath = '') => {
2168
- const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2169
-
2170
- // Skip unsubscribe tag for input fields
2171
- if (tag?.name === UNSUBSCRIBE_TAG_NAME) {
2172
- return;
2173
- }
2174
-
2175
- hasPersonalizationTags = true;
2176
-
2177
- if (tag?.metaData?.userDriven === false) {
2178
- required.push({
2179
- ...tag,
2180
- fullPath: currentPath,
2181
- });
2182
- } else if (tag?.metaData?.userDriven === true) {
2183
- optional.push({
2184
- ...tag,
2185
- fullPath: currentPath,
2186
- });
2187
- }
2188
-
2189
- if (tag?.children?.length > 0) {
2190
- tag.children.forEach((child) => processTag(child, currentPath));
2191
- }
2192
- };
2193
-
2194
- extractedTags.forEach((tag) => processTag(tag));
2195
-
2196
- if (hasPersonalizationTags) {
2197
- setRequiredTags(required);
2198
- setOptionalTags(optional);
2199
- setTagsExtracted(true); // Mark tags as extracted and processed
2200
-
2201
- // Initialize custom values for required tags
2202
- const initialValues = {};
2203
- required.forEach((tag) => {
2204
- initialValues[tag?.fullPath] = '';
2205
- });
2206
- optional.forEach((tag) => {
2207
- initialValues[tag?.fullPath] = '';
2208
- });
2209
- setCustomValues(initialValues);
2210
- } else {
2211
- // Reset all tag-related state if no personalization tags
2212
- setRequiredTags([]);
2213
- setOptionalTags([]);
2214
- setCustomValues({});
2215
- setTagsExtracted(false);
2216
- }
2217
- } else {
2218
- // Reset all tag-related state if no tags
2219
- setRequiredTags([]);
2220
- setOptionalTags([]);
2221
- setCustomValues({});
2222
- setTagsExtracted(false);
2223
- }
2224
- }, [extractedTags]);
2225
-
2226
2658
  /**
2227
2659
  * Handle customer selection and fetch prefilled values
2228
2660
  */
@@ -2230,17 +2662,15 @@ const CommonTestAndPreview = (props) => {
2230
2662
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2231
2663
  setTagsExtracted(true); // Auto-open custom values editor
2232
2664
 
2233
- // Get all available tags
2234
- const allTags = [...requiredTags, ...optionalTags];
2235
2665
  const requiredTagObj = {};
2236
- requiredTags.forEach((tag) => {
2666
+ allRequiredTags.forEach((tag) => {
2237
2667
  requiredTagObj[tag?.fullPath] = '';
2238
2668
  });
2239
2669
  if (allTags.length > 0) {
2240
2670
  const payload = preparePreviewPayload(
2241
- channel,
2671
+ activeChannelForActions,
2242
2672
  formData || {},
2243
- getCurrentContent,
2673
+ activeContentForActions,
2244
2674
  {
2245
2675
  ...requiredTagObj,
2246
2676
  },
@@ -2249,7 +2679,7 @@ const CommonTestAndPreview = (props) => {
2249
2679
  actions.getPrefilledValuesRequested(payload);
2250
2680
  }
2251
2681
  }
2252
- }, [selectedCustomer]);
2682
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2253
2683
 
2254
2684
  /**
2255
2685
  * Update custom values with prefilled values from API
@@ -2260,7 +2690,7 @@ const CommonTestAndPreview = (props) => {
2260
2690
  if (prefilledValues && selectedCustomer) {
2261
2691
  // Always replace all values with prefilled values
2262
2692
  const updatedValues = {};
2263
- [...requiredTags, ...optionalTags].forEach((tag) => {
2693
+ allTags.forEach((tag) => {
2264
2694
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2265
2695
  });
2266
2696
 
@@ -2268,16 +2698,17 @@ const CommonTestAndPreview = (props) => {
2268
2698
 
2269
2699
  // Update preview with prefilled values (this is a valid preview call trigger)
2270
2700
  const payload = preparePreviewPayload(
2271
- channel,
2701
+ activeChannelForActions,
2272
2702
  formData || {},
2273
- getCurrentContent,
2703
+ activeContentForActions,
2274
2704
  updatedValues,
2275
2705
  selectedCustomer
2276
2706
  );
2277
2707
  actions.updatePreviewRequested(payload);
2278
2708
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2709
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2279
2710
  }
2280
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2711
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2281
2712
 
2282
2713
  /**
2283
2714
  * Map channel constants to display names (lowercase for message)
@@ -2370,11 +2801,7 @@ const CommonTestAndPreview = (props) => {
2370
2801
  setTagsExtracted(true); // Auto-open custom values editor
2371
2802
 
2372
2803
  // Clear any existing values while waiting for prefilled values
2373
- const emptyValues = {};
2374
- [...requiredTags, ...optionalTags].forEach((tag) => {
2375
- emptyValues[tag?.fullPath] = '';
2376
- });
2377
- setCustomValues(emptyValues);
2804
+ setCustomValues(buildEmptyValues());
2378
2805
  };
2379
2806
 
2380
2807
  /**
@@ -2388,11 +2815,7 @@ const CommonTestAndPreview = (props) => {
2388
2815
  actions.clearPreviewErrors();
2389
2816
 
2390
2817
  // Initialize empty values for all tags
2391
- const emptyValues = {};
2392
- [...requiredTags, ...optionalTags].forEach((tag) => {
2393
- emptyValues[tag?.fullPath] = '';
2394
- });
2395
- setCustomValues(emptyValues);
2818
+ setCustomValues(buildEmptyValues());
2396
2819
 
2397
2820
  // Don't make preview call when clearing selection - just reset to raw content
2398
2821
  // Preview will be shown using raw formData/content
@@ -2428,17 +2851,17 @@ const CommonTestAndPreview = (props) => {
2428
2851
  */
2429
2852
  const handleDiscardCustomValues = () => {
2430
2853
  // Initialize empty values for all tags
2431
- const emptyValues = {};
2432
- [...requiredTags, ...optionalTags].forEach((tag) => {
2433
- emptyValues[tag?.fullPath] = '';
2434
- });
2854
+ const emptyValues = buildEmptyValues();
2435
2855
  setCustomValues(emptyValues);
2436
2856
 
2857
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
2858
+ setSmsFallbackPreviewText(undefined);
2859
+
2437
2860
  // Update preview with empty values (this is a valid preview call trigger)
2438
2861
  const payload = preparePreviewPayload(
2439
- channel,
2862
+ activeChannelForActions,
2440
2863
  formData || {},
2441
- getCurrentContent,
2864
+ activeContentForActions,
2442
2865
  emptyValues,
2443
2866
  selectedCustomer
2444
2867
  );
@@ -2453,13 +2876,15 @@ const CommonTestAndPreview = (props) => {
2453
2876
  const handleUpdatePreview = async () => {
2454
2877
  try {
2455
2878
  const payload = preparePreviewPayload(
2456
- channel,
2879
+ activeChannelForActions,
2457
2880
  formData || {},
2458
- getCurrentContent,
2881
+ activeContentForActions,
2459
2882
  customValues,
2460
2883
  selectedCustomer
2461
2884
  );
2462
2885
  await actions.updatePreviewRequested(payload);
2886
+
2887
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2463
2888
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2464
2889
  } catch (error) {
2465
2890
  CapNotification.error({
@@ -2469,25 +2894,115 @@ const CommonTestAndPreview = (props) => {
2469
2894
  };
2470
2895
 
2471
2896
  /**
2472
- * Handle extract tags
2897
+ * Categorize extracted tags into required/optional.
2473
2898
  */
2474
- const handleExtractTags = () => {
2475
- // Get content based on channel
2476
- let contentToExtract = getCurrentContent;
2899
+ const categorizeTags = (tagsTree = []) => {
2900
+ const required = [];
2901
+ const optional = [];
2902
+ let hasPersonalizationTags = false;
2903
+ const processTag = (tag, parentPath = '') => {
2904
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2905
+
2906
+ // Skip unsubscribe tag for input fields
2907
+ if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
2908
+
2909
+ hasPersonalizationTags = true;
2910
+ const userDriven = tag?.metaData?.userDriven;
2911
+ if (userDriven === true) {
2912
+ optional.push({ ...tag, fullPath: currentPath });
2913
+ } else {
2914
+ // false or missing (SMS/DLT extract often omits metaData) → required for test values
2915
+ required.push({ ...tag, fullPath: currentPath });
2916
+ }
2917
+
2918
+ if (tag?.children?.length > 0) {
2919
+ tag.children.forEach((child) => processTag(child, currentPath));
2920
+ }
2921
+ };
2922
+
2923
+ (tagsTree || []).forEach((tag) => processTag(tag));
2924
+ return { required, optional, hasPersonalizationTags };
2925
+ };
2926
+
2927
+ /**
2928
+ * Apply tag extraction when content comes from RCS + SMS fallback (no API call).
2929
+ */
2930
+ const applyRcsSmsFallbackTagExtraction = () => {
2931
+ const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
2932
+ const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
2933
+ let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
2934
+ ? smsFallbackExtractedTags
2935
+ : buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
2936
+ if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
2937
+ fallbackSmsTagTree = [];
2938
+ }
2939
+ const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
2940
+ setRequiredTags(rcsPrimaryCategorized.required);
2941
+ setOptionalTags(rcsPrimaryCategorized.optional);
2942
+ setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
2943
+ setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
2944
+ setTagsExtracted(
2945
+ rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
2946
+ );
2947
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
2948
+ };
2949
+
2950
+ /**
2951
+ * When extract-tags API returns, map Redux `extractedTags` into required/optional so
2952
+ * CustomValuesEditor shows personalization fields (effect was previously commented out).
2953
+ * RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
2954
+ */
2955
+ useEffect(() => {
2956
+ if (!show) return;
2957
+ const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
2958
+ if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
2959
+ applyRcsSmsFallbackTagExtraction();
2960
+ return;
2961
+ }
2962
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
2963
+ let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
2964
+ ? buildSyntheticSmsMustacheTags(smsEditorBody)
2965
+ : (extractedTags ?? []);
2966
+ if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
2967
+ smsTagSource = [];
2968
+ }
2969
+ const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
2970
+ setRequiredTags(required);
2971
+ setOptionalTags(optional);
2972
+ setTagsExtracted(hasPersonalizationTags);
2973
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
2974
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
2975
+ }, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
2477
2976
 
2977
+ /**
2978
+ * Get content to run tag extraction on (channel-specific).
2979
+ */
2980
+ const getContentForTagExtraction = () => {
2981
+ let contentToExtract = activeContentForActions;
2478
2982
  if (channel === CHANNELS.EMAIL && formData) {
2479
2983
  const currentTabData = formData[currentTab - 1];
2480
2984
  const activeTab = currentTabData?.activeTab;
2481
2985
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2482
2986
  contentToExtract = templateContent || contentToExtract;
2483
2987
  }
2988
+ return contentToExtract;
2989
+ };
2484
2990
 
2485
- // Check for personalization tags (excluding unsubscribe)
2991
+ /**
2992
+ * Handle extract tags
2993
+ */
2994
+ const handleExtractTags = () => {
2995
+ if (channel === CHANNELS.RCS) {
2996
+ applyRcsSmsFallbackTagExtraction();
2997
+ return;
2998
+ }
2999
+
3000
+ const contentToExtract = getContentForTagExtraction();
2486
3001
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2487
3002
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3003
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2488
3004
 
2489
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2490
- // If only unsubscribe tag is present, show noTagsExtracted message
3005
+ if (onlyUnsubscribe) {
2491
3006
  setTagsExtracted(false);
2492
3007
  setRequiredTags([]);
2493
3008
  setOptionalTags([]);
@@ -2495,10 +3010,9 @@ const CommonTestAndPreview = (props) => {
2495
3010
  return;
2496
3011
  }
2497
3012
 
2498
- // Extract tags
2499
3013
  setTagsExtracted(true);
2500
3014
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2501
- channel,
3015
+ activeChannelForActions,
2502
3016
  formData || {},
2503
3017
  contentToExtract
2504
3018
  );
@@ -2518,7 +3032,7 @@ const CommonTestAndPreview = (props) => {
2518
3032
  const handleSendTestMessage = () => {
2519
3033
  const allUserIds = [];
2520
3034
  selectedTestEntities.forEach((entityId) => {
2521
- const group = testGroups.find((g) => g.groupId === entityId);
3035
+ const group = testGroups.find((testGroup) => testGroup.groupId === entityId);
2522
3036
  if (group) {
2523
3037
  allUserIds.push(...group.userIds);
2524
3038
  } else {
@@ -2527,11 +3041,12 @@ const CommonTestAndPreview = (props) => {
2527
3041
  });
2528
3042
  const uniqueUserIds = [...new Set(allUserIds)];
2529
3043
 
2530
- const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
3044
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
2531
3045
  ? testPreviewDeliverySettings[channel]
2532
3046
  : null;
2533
3047
 
2534
- // Create initial payload based on channel
3048
+ // createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
3049
+ // Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
2535
3050
  const initialPayload = prepareTestMessagePayload(
2536
3051
  channel,
2537
3052
  formData || content || {},
@@ -2539,7 +3054,16 @@ const CommonTestAndPreview = (props) => {
2539
3054
  customValues,
2540
3055
  uniqueUserIds,
2541
3056
  previewData,
2542
- deliveryOverride
3057
+ deliveryOverride,
3058
+ channel === CHANNELS.RCS
3059
+ ? {
3060
+ smsFallbackTemplateContent:
3061
+ smsFallbackTextForTagExtraction
3062
+ || smsFallbackContent?.templateContent
3063
+ || smsFallbackContent?.content
3064
+ || '',
3065
+ }
3066
+ : {},
2543
3067
  );
2544
3068
 
2545
3069
  actions.createMessageMetaRequested(
@@ -2572,11 +3096,10 @@ const CommonTestAndPreview = (props) => {
2572
3096
  // ============================================
2573
3097
  // RENDER HELPER FUNCTIONS
2574
3098
  // ============================================
2575
-
2576
3099
  const renderLeftPanelContent = () => (
2577
3100
  <LeftPanelContent
2578
- isExtractingTags={isExtractingTags}
2579
- extractedTags={extractedTags}
3101
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3102
+ extractedTags={leftPanelExtractedTags}
2580
3103
  selectedCustomer={selectedCustomer}
2581
3104
  handleCustomerSelect={handleCustomerSelect}
2582
3105
  handleSearchCustomer={handleSearchCustomer}
@@ -2594,15 +3117,29 @@ const CommonTestAndPreview = (props) => {
2594
3117
 
2595
3118
  const renderCustomValuesEditor = () => (
2596
3119
  <CustomValuesEditor
2597
- isExtractingTags={isExtractingTags}
3120
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
2598
3121
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
2599
3122
  showJSON={showJSON}
2600
3123
  setShowJSON={setShowJSON}
2601
3124
  customValues={customValues}
2602
3125
  handleJSONTextChange={handleJSONTextChange}
2603
- extractedTags={extractedTags}
2604
- requiredTags={requiredTags}
2605
- optionalTags={optionalTags}
3126
+ sections={[
3127
+ {
3128
+ key: channel,
3129
+ title:
3130
+ channel === CHANNELS.RCS
3131
+ ? messages.rcsTagsSectionTitle
3132
+ : messages[`${channel}TagsSectionTitle`],
3133
+ requiredTags,
3134
+ optionalTags,
3135
+ },
3136
+ {
3137
+ key: PREVIEW_TAB_SMS_FALLBACK,
3138
+ title: channel === CHANNELS.RCS && smsFallbackContent?.templateContent ? messages.smsFallbackTagsSectionTitle : null,
3139
+ requiredTags: smsFallbackRequiredTags,
3140
+ optionalTags: smsFallbackOptionalTags,
3141
+ },
3142
+ ]}
2606
3143
  handleCustomValueChange={handleCustomValueChange}
2607
3144
  handleDiscardCustomValues={handleDiscardCustomValues}
2608
3145
  handleUpdatePreview={handleUpdatePreview}
@@ -2632,12 +3169,13 @@ const CommonTestAndPreview = (props) => {
2632
3169
  isSendingTestMessage={isSendingTestMessage}
2633
3170
  formatMessage={formatMessage}
2634
3171
  deliverySettings={testPreviewDeliverySettings[channel]}
2635
- senderDetailsOptions={senderDetailsByChannel[channel]}
3172
+ senderDetailsByChannel={senderDetailsByChannel}
2636
3173
  wecrmAccounts={wecrmAccounts}
2637
3174
  onSaveDeliverySettings={handleSaveDeliverySettings}
2638
3175
  isLoadingSenderDetails={isLoadingSenderDetails}
2639
3176
  smsTraiDltEnabled={smsTraiDltEnabled}
2640
3177
  registeredSenderIds={registeredSenderIds}
3178
+ isChannelSmsFallbackPreviewEnabled={channel === CHANNELS.RCS && !!smsFallbackContent?.templateContent}
2641
3179
  />
2642
3180
  );
2643
3181