@capillarytech/creatives-library 8.0.345-alpha.13 → 8.0.345-alpha.15

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 (138) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/api.js +0 -20
  4. package/services/tests/api.test.js +13 -59
  5. package/utils/commonUtils.js +19 -1
  6. package/utils/rcsPayloadUtils.js +92 -0
  7. package/utils/templateVarUtils.js +201 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +167 -109
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapCustomSkeleton/index.js +1 -1
  15. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  16. package/v2Components/CapTagList/index.js +10 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +676 -186
  32. package/v2Components/CommonTestAndPreview/messages.js +49 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  46. package/v2Components/FormBuilder/index.js +8 -10
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +955 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -28
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  71. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  72. package/v2Containers/CreativesContainer/SlideBoxFooter.js +11 -4
  73. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  74. package/v2Containers/CreativesContainer/constants.js +9 -0
  75. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  76. package/v2Containers/CreativesContainer/index.js +300 -108
  77. package/v2Containers/CreativesContainer/index.scss +51 -1
  78. package/v2Containers/CreativesContainer/messages.js +0 -4
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -18
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  88. package/v2Containers/Rcs/constants.js +119 -8
  89. package/v2Containers/Rcs/index.js +2379 -807
  90. package/v2Containers/Rcs/index.js.rej +1336 -0
  91. package/v2Containers/Rcs/index.scss +276 -6
  92. package/v2Containers/Rcs/index.scss.rej +74 -0
  93. package/v2Containers/Rcs/messages.js +38 -3
  94. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  95. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  96. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  97. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  98. package/v2Containers/Rcs/tests/index.test.js +152 -121
  99. package/v2Containers/Rcs/tests/mockData.js +38 -0
  100. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  101. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  102. package/v2Containers/Rcs/utils.js +478 -11
  103. package/v2Containers/Sms/Create/index.js +100 -40
  104. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  105. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  106. package/v2Containers/SmsTrai/Create/index.js +9 -4
  107. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  108. package/v2Containers/SmsTrai/Edit/index.js +636 -130
  109. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  110. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  111. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  112. package/v2Containers/SmsWrapper/index.js +37 -8
  113. package/v2Containers/TagList/index.js +6 -0
  114. package/v2Containers/Templates/ChannelTypeIllustration.js +6 -23
  115. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  116. package/v2Containers/Templates/_templates.scss +181 -126
  117. package/v2Containers/Templates/actions.js +11 -36
  118. package/v2Containers/Templates/constants.js +2 -23
  119. package/v2Containers/Templates/index.js +142 -333
  120. package/v2Containers/Templates/messages.js +0 -68
  121. package/v2Containers/Templates/reducer.js +0 -68
  122. package/v2Containers/Templates/sagas.js +55 -98
  123. package/v2Containers/Templates/selectors.js +0 -12
  124. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +0 -12
  125. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  126. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1042 -1256
  127. package/v2Containers/Templates/tests/index.test.js +0 -6
  128. package/v2Containers/Templates/tests/reducer.test.js +0 -178
  129. package/v2Containers/Templates/tests/sagas.test.js +200 -436
  130. package/v2Containers/Templates/tests/selector.test.js +0 -32
  131. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  132. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  133. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  134. package/v2Containers/TemplatesV2/index.js +86 -23
  135. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  136. package/v2Containers/Whatsapp/index.js +3 -20
  137. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  138. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +0 -9
@@ -31,7 +31,8 @@ import CustomValuesEditor from './CustomValuesEditor';
31
31
  import SendTestMessage from './SendTestMessage';
32
32
  import PreviewSection from './PreviewSection';
33
33
 
34
- // Import constants
34
+ import * as Api from '../../services/api';
35
+ import { extractTemplateVariables } from '../../utils/templateVarUtils';
35
36
  import AddTestCustomerButton from './AddTestCustomer';
36
37
  import ExistingCustomerModal from './ExistingCustomerModal';
37
38
  // Import constants
@@ -81,11 +82,23 @@ import {
81
82
  IMAGE,
82
83
  VIDEO,
83
84
  URL,
84
- CHANNELS_USING_ANDROID_PREVIEW_DEVICE
85
+ PREVIEW_TAB_RCS,
86
+ PREVIEW_TAB_SMS_FALLBACK,
87
+ CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
88
+ RCS_TEST_META_CONTENT_TYPE_RICHCARD,
89
+ RCS_TEST_META_CARD_TYPE_STANDALONE,
90
+ RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
91
+ RCS_TEST_META_CARD_WIDTH_SMALL,
92
+ SMS_MUSTACHE_TAG_PATTERN,
85
93
  } from './constants';
86
-
87
- // Import utilities
88
94
  import { getCdnUrl } from '../../utils/cdnTransformation';
95
+ import {
96
+ normalizePreviewApiPayload,
97
+ extractPreviewFromLiquidResponse,
98
+ getSmsFallbackTextForTagExtraction,
99
+ } from './previewApiUtils';
100
+ import { pickFirstSmsFallbackTemplateString } from '../../v2Containers/Rcs/rcsLibraryHydrationUtils';
101
+
89
102
  import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
90
103
  import { getMembersLookup } from '../../services/api';
91
104
 
@@ -108,6 +121,85 @@ const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEch
108
121
  });
109
122
  };
110
123
 
124
+ /** Preview payload from Redux may be an Immutable Map — normalize for React state. */
125
+ const toPlainPreviewData = (data) => {
126
+ if (data == null) return null;
127
+ const plain = typeof data.toJS === 'function' ? data.toJS() : data;
128
+ return normalizePreviewApiPayload(plain);
129
+ };
130
+
131
+ /**
132
+ * Merge existing customValues with tag keys from categorized groups.
133
+ * Each group is { required, optional } (arrays of tag objects with fullPath).
134
+ * Preserves existing values; adds '' for any tag key not yet present.
135
+ * Reusable for RCS+fallback and any flow that needs to ensure customValues has keys for tags.
136
+ */
137
+ const mergeCustomValuesWithTagKeys = (prev, ...categorizedGroups) => {
138
+ const next = { ...(prev || {}) };
139
+ categorizedGroups.forEach((group) => {
140
+ [...(group.required || []), ...(group.optional || [])].forEach((tag) => {
141
+ const key = tag?.fullPath;
142
+ if (key && next[key] === undefined) next[key] = '';
143
+ });
144
+ });
145
+ return next;
146
+ };
147
+
148
+ /** True when `body` contains `{{name}}` mustache tokens (user-fillable personalization tags).
149
+ * DLT `{#name#}` slots are pre-bound template variables and are intentionally excluded. */
150
+ const smsTemplateHasMustacheTags = (body) =>
151
+ typeof body === 'string' && SMS_MUSTACHE_TAG_PATTERN.test(body);
152
+
153
+ /**
154
+ * Build tag rows from `{{…}}` mustache tokens only — DLT `{#…#}` slots are excluded because
155
+ * they are pre-bound template variables, not user-fillable personalization tags.
156
+ * Passing a mustache-only captureRegex to extractTemplateVariables skips the DLT branch.
157
+ * A non-global regex is used so ensureGlobalRegexForExecLoop creates a fresh instance on each call.
158
+ */
159
+ const buildSyntheticSmsMustacheTags = (body = '') => {
160
+ if (!body || typeof body !== 'string') return [];
161
+ return extractTemplateVariables(body, /\{\{([^}]+)\}\}/).map((name) => ({
162
+ name,
163
+ metaData: { userDriven: false },
164
+ children: [],
165
+ }));
166
+ };
167
+
168
+ /** RCS createMessageMeta: media shape (mediaUrl, thumbnailUrl, height string). */
169
+ const normalizeRcsTestCardMedia = (media) => {
170
+ if (!media || typeof media !== 'object') return undefined;
171
+ const mediaUrl =
172
+ media.mediaUrl != null && String(media.mediaUrl).trim() !== ''
173
+ ? String(media.mediaUrl)
174
+ : media.url != null && String(media.url).trim() !== ''
175
+ ? String(media.url)
176
+ : '';
177
+ const thumbnailUrl = media.thumbnailUrl != null ? String(media.thumbnailUrl) : '';
178
+ const height = media.height != null ? String(media.height) : undefined;
179
+ const out = { mediaUrl, thumbnailUrl };
180
+ if (height) out.height = height;
181
+ return out;
182
+ };
183
+
184
+ /** RCS createMessageMeta: suggestion shape (index, type, text, phoneNumber, url, postback). */
185
+ const mapRcsSuggestionForTestMeta = (suggestionRow, index) => ({
186
+ index,
187
+ type: suggestionRow?.type ?? '',
188
+ text: suggestionRow?.text != null ? String(suggestionRow.text) : '',
189
+ phoneNumber:
190
+ suggestionRow?.phoneNumber != null
191
+ ? String(suggestionRow.phoneNumber)
192
+ : suggestionRow?.phone_number != null
193
+ ? String(suggestionRow.phone_number)
194
+ : '',
195
+ url: suggestionRow?.url !== undefined ? suggestionRow.url : null,
196
+ postback:
197
+ suggestionRow?.postback != null
198
+ ? String(suggestionRow.postback)
199
+ : suggestionRow?.text != null
200
+ ? String(suggestionRow.text)
201
+ : '',
202
+ });
111
203
 
112
204
  /**
113
205
  * CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
@@ -133,7 +225,7 @@ const testEntityIdsEqual = (a, b) => {
133
225
  */
134
226
  const CommonTestAndPreview = (props) => {
135
227
  const {
136
- intl: { formatMessage },
228
+ intl: { formatMessage, locale: userLocale = 'en' },
137
229
  show,
138
230
  onClose,
139
231
  channel, // The channel: 'EMAIL', 'SMS', 'RCS', etc.
@@ -169,12 +261,21 @@ const CommonTestAndPreview = (props) => {
169
261
  ...additionalProps
170
262
  } = props;
171
263
 
264
+ const smsFallbackContent = additionalProps?.smsFallbackContent;
265
+ const smsFallbackTextForTagExtraction = useMemo(
266
+ () => getSmsFallbackTextForTagExtraction(smsFallbackContent),
267
+ [smsFallbackContent],
268
+ );
172
269
  // ============================================
173
270
  // STATE MANAGEMENT
174
271
  // ============================================
175
272
  const [selectedCustomer, setSelectedCustomer] = useState(null);
176
273
  const [requiredTags, setRequiredTags] = useState([]);
177
274
  const [optionalTags, setOptionalTags] = useState([]);
275
+ const [smsFallbackExtractedTags, setSmsFallbackExtractedTags] = useState([]);
276
+ const [smsFallbackRequiredTags, setSmsFallbackRequiredTags] = useState([]);
277
+ const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
278
+ const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
178
279
  const [customValues, setCustomValues] = useState({});
179
280
  const [showJSON, setShowJSON] = useState(false);
180
281
  const [tagsExtracted, setTagsExtracted] = useState(false);
@@ -186,6 +287,8 @@ const CommonTestAndPreview = (props) => {
186
287
  const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
187
288
 
188
289
  const [previewDevice, setPreviewDevice] = useState(initialDevice);
290
+ const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
291
+ const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
189
292
  // Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
190
293
  const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
191
294
  const [previewDataHtml, setPreviewDataHtml] = useState(() => {
@@ -218,15 +321,22 @@ const CommonTestAndPreview = (props) => {
218
321
  [CHANNELS.WHATSAPP]: {
219
322
  domainId: null, senderMobNum: '', sourceAccountIdentifier: '',
220
323
  },
324
+ [CHANNELS.RCS]: {
325
+ domainId: null,
326
+ domainGatewayMapId: null,
327
+ gsmSenderId: '',
328
+ smsFallbackDomainId: null,
329
+ cdmaSenderId: '', // gsmSenderId = RCS sender (domainId|senderId), cdmaSenderId = SMS fallback
330
+ },
221
331
  });
222
332
 
223
- const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP];
333
+ const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
224
334
  const formDataForSendTest = formData ?? (content && typeof content === 'object' && !Array.isArray(content) ? content : formData);
225
335
  const smsTemplateConfigs = formDataForSendTest?.templateConfigs || {};
226
336
  const smsTraiDltEnabled = !!smsTemplateConfigs?.traiDltEnabled;
227
337
  const registeredSenderIds = smsTemplateConfigs?.registeredSenderIds || [];
228
338
 
229
- // Fetch sender details and WeCRM accounts when Test & Preview opens for SMS/Email/WhatsApp
339
+ // Fetch sender details and WeCRM accounts when Test & Preview opens (SMS, Email, WhatsApp, RCS — same process)
230
340
  useEffect(() => {
231
341
  if (!show || !channel) {
232
342
  return;
@@ -235,39 +345,99 @@ const CommonTestAndPreview = (props) => {
235
345
  if (actions.getSenderDetailsRequested) {
236
346
  actions.getSenderDetailsRequested({ channel, orgUnitId: orgUnitId ?? -1 });
237
347
  }
348
+ // SMS domains/senders are needed for RCS delivery UI (fallback row + slidebox) whenever RCS is open — not only when fallback body exists.
349
+ if (channel === CHANNELS.RCS && actions.getSenderDetailsRequested) {
350
+ actions.getSenderDetailsRequested({ channel: CHANNELS.SMS, orgUnitId: orgUnitId ?? -1 });
351
+ }
238
352
  if (channel === CHANNELS.WHATSAPP && actions.getWeCrmAccountsRequested) {
239
353
  actions.getWeCrmAccountsRequested({ sourceName: CHANNELS.WHATSAPP });
240
354
  }
241
355
  }
242
356
  }, [show, channel, orgUnitId, actions]);
243
357
 
244
- useEffect(() => {
245
- if (!show) {
246
- setCustomerModal([false, '']);
247
- setSearchValue('');
248
- setCustomerData({ name: '', email: '', mobile: '', customerId: '' });
249
- }
250
- }, [show]);
251
-
252
358
  const findDefault = (arr) => (arr && arr.find((x) => x.default)) || (arr && arr[0]) || {};
253
359
 
254
360
  // Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
255
361
  useEffect(() => {
256
362
  if (!channel || !channelsWithDeliverySettings.includes(channel)) return;
363
+
364
+ if (channel === CHANNELS.RCS) {
365
+ const rcsDomainRows = senderDetailsByChannel?.[CHANNELS.RCS] || [];
366
+ const smsFallbackDomainRows = senderDetailsByChannel?.[CHANNELS.SMS] || [];
367
+ if (!rcsDomainRows.length) return;
368
+
369
+ const currentRcsDeliverySettings = testPreviewDeliverySettings?.[CHANNELS.RCS] || {};
370
+ const isRcsGsmSenderUnset = !currentRcsDeliverySettings?.gsmSenderId;
371
+ const isSmsFallbackSenderUnset = !currentRcsDeliverySettings?.cdmaSenderId;
372
+ const isRcsDeliveryFullyUnset = isRcsGsmSenderUnset && isSmsFallbackSenderUnset;
373
+ const shouldOnlyFillSmsFallbackSender =
374
+ !isRcsGsmSenderUnset && isSmsFallbackSenderUnset && smsFallbackDomainRows.length > 0;
375
+
376
+ if (!isRcsDeliveryFullyUnset && !shouldOnlyFillSmsFallbackSender) return;
377
+
378
+ const firstRcsDomain = rcsDomainRows[0];
379
+ const firstSmsFallbackDomain = smsFallbackDomainRows[0];
380
+ const usableRcsGsmSenders = filterUsableGsmSendersForDomain(
381
+ firstRcsDomain,
382
+ firstRcsDomain?.gsmSenders,
383
+ { skipDomainNameEchoFilter: true },
384
+ );
385
+ const usableSmsFallbackGsmSenders = firstSmsFallbackDomain
386
+ ? filterUsableGsmSendersForDomain(firstSmsFallbackDomain, firstSmsFallbackDomain?.gsmSenders)
387
+ : [];
388
+ const defaultRcsGsmSender = usableRcsGsmSenders[0];
389
+ const defaultSmsFallbackGsmSender = usableSmsFallbackGsmSenders[0];
390
+ const rcsSenderCompositeValue =
391
+ firstRcsDomain?.domainId != null && defaultRcsGsmSender?.value != null
392
+ ? `${firstRcsDomain.domainId}|${defaultRcsGsmSender.value}`
393
+ : (defaultRcsGsmSender?.value || '');
394
+ const smsFallbackSenderCompositeValue =
395
+ firstSmsFallbackDomain?.domainId != null && defaultSmsFallbackGsmSender?.value != null
396
+ ? `${firstSmsFallbackDomain.domainId}|${defaultSmsFallbackGsmSender.value}`
397
+ : (defaultSmsFallbackGsmSender?.value || '');
398
+
399
+ setTestPreviewDeliverySettings((prev) => {
400
+ const previousRcsSettings = prev?.[CHANNELS.RCS] || {};
401
+ if (shouldOnlyFillSmsFallbackSender) {
402
+ if (!smsFallbackSenderCompositeValue) return prev;
403
+ return {
404
+ ...prev,
405
+ [CHANNELS.RCS]: {
406
+ ...previousRcsSettings,
407
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
408
+ cdmaSenderId: smsFallbackSenderCompositeValue,
409
+ },
410
+ };
411
+ }
412
+ return {
413
+ ...prev,
414
+ [CHANNELS.RCS]: {
415
+ domainId: firstRcsDomain?.domainId ?? null,
416
+ domainGatewayMapId: firstRcsDomain?.dgmId ?? null,
417
+ gsmSenderId: rcsSenderCompositeValue,
418
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
419
+ cdmaSenderId: smsFallbackSenderCompositeValue,
420
+ },
421
+ };
422
+ });
423
+ return;
424
+ }
425
+
257
426
  const domains = senderDetailsByChannel[channel];
258
427
  if (!domains || domains.length === 0) return;
259
428
  const {
260
- domainId = '', gsmSenderId = '', senderEmail = '', senderMobNum = '',
429
+ domainId = '', gsmSenderId = '', cdmaSenderId = '', senderEmail = '', senderMobNum = '',
261
430
  } = testPreviewDeliverySettings[channel] || {};
262
- const isEmptySelection = !domainId && !gsmSenderId && !senderEmail && !senderMobNum;
431
+ const isEmptySelection = !domainId && !gsmSenderId && !cdmaSenderId && !senderEmail && !senderMobNum;
263
432
  if (!isEmptySelection) return;
264
433
 
265
434
  const whatsappAccountFromForm = channel === CHANNELS.WHATSAPP ? formData?.accountName : undefined;
266
435
  const matchedWhatsappAccount = whatsappAccountFromForm
267
436
  ? (wecrmAccounts || []).find((account) => account?.name === whatsappAccountFromForm)
268
437
  : null;
269
- const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled
270
- ? domains.filter((domain) => (domain.gsmSenders || []).some((gsm) => registeredSenderIds.includes(gsm.value)))
438
+ const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled && registeredSenderIds?.length
439
+ ? domains.filter((domain) => (domain?.gsmSenders || []).some((gsm) =>
440
+ registeredSenderIds?.includes(gsm?.value)))
271
441
  : domains;
272
442
  const [defaultDomain] = domains;
273
443
  const [firstSmsDomain] = smsDomains;
@@ -282,8 +452,8 @@ const CommonTestAndPreview = (props) => {
282
452
  const next = { ...prev };
283
453
  if (channel === CHANNELS.SMS) {
284
454
  const smsGsmSenders = smsTraiDltEnabled
285
- ? (firstDomain.gsmSenders || []).filter((gsm) => registeredSenderIds.includes(gsm.value))
286
- : firstDomain.gsmSenders;
455
+ ? (firstDomain?.gsmSenders || []).filter((gsm) => registeredSenderIds?.includes(gsm?.value))
456
+ : firstDomain?.gsmSenders;
287
457
  next[channel] = {
288
458
  domainId: firstDomain.domainId,
289
459
  domainGatewayMapId: firstDomain.dgmId,
@@ -316,10 +486,29 @@ const CommonTestAndPreview = (props) => {
316
486
  // MEMOIZED VALUES
317
487
  // ============================================
318
488
 
489
+ const allTags = useMemo(
490
+ () => [...requiredTags, ...optionalTags, ...smsFallbackRequiredTags, ...smsFallbackOptionalTags],
491
+ [requiredTags, optionalTags, smsFallbackRequiredTags, smsFallbackOptionalTags]
492
+ );
493
+
494
+ const allRequiredTags = useMemo(
495
+ () => [...requiredTags, ...smsFallbackRequiredTags],
496
+ [requiredTags, smsFallbackRequiredTags]
497
+ );
498
+
499
+ const buildEmptyValues = useCallback(
500
+ () => allTags.reduce((acc, tag) => {
501
+ const key = tag?.fullPath;
502
+ if (key) acc[key] = '';
503
+ return acc;
504
+ }, {}),
505
+ [allTags]
506
+ );
507
+
319
508
  // Check if update preview button should be disabled
320
509
  const isUpdatePreviewDisabled = useMemo(() => (
321
- requiredTags.some((tag) => !customValues[tag.fullPath])
322
- ), [requiredTags, customValues]);
510
+ allRequiredTags.some((tag) => !customValues[tag.fullPath])
511
+ ), [allRequiredTags, customValues]);
323
512
 
324
513
  // Get current content based on channel and editor type
325
514
  const getCurrentContent = useMemo(() => {
@@ -363,6 +552,13 @@ const CommonTestAndPreview = (props) => {
363
552
  return currentTabData.base['sms-editor'];
364
553
  }
365
554
  }
555
+ // DLT / Test & Preview shape: { templateConfigs: { template, templateId, ... } }
556
+ if (formData.templateConfigs?.template) {
557
+ const smsDltTemplateValue = formData.templateConfigs.template;
558
+ if (typeof smsDltTemplateValue === 'string') return smsDltTemplateValue;
559
+ if (Array.isArray(smsDltTemplateValue)) return smsDltTemplateValue.join('');
560
+ return '';
561
+ }
366
562
  }
367
563
 
368
564
  // SMS channel fallback - if formData is not available, use content directly
@@ -408,7 +604,70 @@ const CommonTestAndPreview = (props) => {
408
604
  return content || '';
409
605
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
410
606
 
411
- // Build test entities tree data
607
+ const leftPanelExtractedTags = useMemo(() => {
608
+ if (channel === CHANNELS.SMS) {
609
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
610
+ if (!smsTemplateHasMustacheTags(smsEditorBody)) return [];
611
+ const extractTagsFromApi = extractedTags ?? [];
612
+ if (extractTagsFromApi.length > 0) return extractTagsFromApi;
613
+ return buildSyntheticSmsMustacheTags(smsEditorBody);
614
+ }
615
+ const hasFallbackSmsBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
616
+ if (channel === CHANNELS.RCS && hasFallbackSmsBody) {
617
+ const rcsPrimaryTags = extractedTags ?? [];
618
+ const fallbackSmsTextForTags = smsFallbackTextForTagExtraction ?? '';
619
+ const fallbackSmsTagRows = smsTemplateHasMustacheTags(fallbackSmsTextForTags)
620
+ ? (smsFallbackExtractedTags?.length > 0
621
+ ? smsFallbackExtractedTags
622
+ : buildSyntheticSmsMustacheTags(fallbackSmsTextForTags))
623
+ : [];
624
+ const mergedRcsAndFallbackTags = [...rcsPrimaryTags, ...fallbackSmsTagRows];
625
+ if (mergedRcsAndFallbackTags.length > 0) return mergedRcsAndFallbackTags;
626
+ return buildSyntheticSmsMustacheTags(fallbackSmsTextForTags);
627
+ }
628
+ return extractedTags ?? [];
629
+ }, [
630
+ channel,
631
+ extractedTags,
632
+ getCurrentContent,
633
+ smsFallbackContent,
634
+ smsFallbackExtractedTags,
635
+ smsFallbackTextForTagExtraction,
636
+ ]);
637
+
638
+ const isRcsSmsFallbackPreviewEnabled =
639
+ channel === CHANNELS.RCS
640
+ && !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
641
+ // Only treat as SMS when user is on the Fallback SMS tab — not whenever fallback exists (RCS tab needs RCS preview API).
642
+ const isSmsFallbackTabActive = isRcsSmsFallbackPreviewEnabled && activePreviewTab === PREVIEW_TAB_SMS_FALLBACK;
643
+ const activeChannelForActions = isSmsFallbackTabActive ? CHANNELS.SMS : channel;
644
+ // VarSegment slot values live in rcsSmsFallbackVarMapped; raw templateContent alone is stale for /preview Body.
645
+ const resolvedSmsFallbackBodyForPreviewTab =
646
+ smsFallbackTextForTagExtraction
647
+ || smsFallbackContent?.templateContent
648
+ || smsFallbackContent?.content
649
+ || '';
650
+ const activeContentForActions = isSmsFallbackTabActive
651
+ ? resolvedSmsFallbackBodyForPreviewTab
652
+ : getCurrentContent;
653
+
654
+ /**
655
+ * SMS fallback pane must show /preview API result when user updated preview on that tab (plain text).
656
+ * Skip when resolvedBody is RCS-shaped (e.g. user last previewed on RCS tab).
657
+ */
658
+ // smsFallbackPreviewText is the single source of truth for the resolved SMS fallback preview.
659
+ // It is set only by syncSmsFallbackPreview (called from handleUpdatePreview and the
660
+ // prefilled-values effect) and reset to undefined on discard / slidebox close.
661
+ // Using previewDataHtml as a fallback is unsafe because that state is shared with the primary
662
+ // RCS preview and can contain stale SMS or RCS content.
663
+ const smsFallbackResolvedText = useMemo(() => {
664
+ const hasFallbackBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
665
+ if (channel !== CHANNELS.RCS || !hasFallbackBody) return undefined;
666
+ if (smsFallbackPreviewText != null) return smsFallbackPreviewText;
667
+ return undefined;
668
+ }, [channel, smsFallbackContent, smsFallbackPreviewText]);
669
+
670
+ // Build test entities tree data from testCustomers prop
412
671
  // Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
413
672
  const testEntitiesTreeData = useMemo(() => {
414
673
  const groupsNode = {
@@ -417,7 +676,7 @@ const CommonTestAndPreview = (props) => {
417
676
  selectable: false,
418
677
  children: testGroups?.map((group) => ({
419
678
  title: group?.groupName,
420
- value: 'group:' + normalizeTestEntityId(group?.groupId),
679
+ value: normalizeTestEntityId(group?.groupId),
421
680
  })),
422
681
  };
423
682
 
@@ -427,7 +686,7 @@ const CommonTestAndPreview = (props) => {
427
686
  selectable: false,
428
687
  children: testCustomers?.map((customer) => ({
429
688
  title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
430
- value: 'customer:' + normalizeTestEntityId(customer?.userId ?? customer?.customerId),
689
+ value: normalizeTestEntityId(customer?.userId ?? customer?.customerId),
431
690
  })) || [],
432
691
  };
433
692
 
@@ -507,11 +766,10 @@ const CommonTestAndPreview = (props) => {
507
766
  email: customerData?.email || '',
508
767
  mobile: customerData?.mobile || '',
509
768
  });
510
- const prefixedAddedId = 'customer:' + normalizedAddedId;
511
769
  setSelectedTestEntities((prev) => (
512
- prev.some((id) => id === prefixedAddedId)
770
+ prev.some((id) => testEntityIdsEqual(id, normalizedAddedId))
513
771
  ? prev
514
- : [...prev, prefixedAddedId]
772
+ : [...prev, normalizedAddedId]
515
773
  ));
516
774
  }
517
775
  handleCloseCustomerModal();
@@ -615,6 +873,37 @@ const CommonTestAndPreview = (props) => {
615
873
  }
616
874
  };
617
875
 
876
+ /**
877
+ * When RCS has SMS fallback, refresh fallback preview text via the same Liquid /preview API
878
+ * (separate call with SMS channel + fallback template body). Used after primary preview updates.
879
+ */
880
+ const syncSmsFallbackPreview = async (customValuesForResolve, selectedCustomerObj) => {
881
+ const fallbackBodyForLiquidPreview =
882
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
883
+ || smsFallbackContent?.templateContent
884
+ || smsFallbackContent?.content
885
+ || '';
886
+ if (channel !== CHANNELS.RCS || !String(fallbackBodyForLiquidPreview).trim()) return;
887
+ try {
888
+ const smsFallbackPayload = preparePreviewPayload(
889
+ CHANNELS.SMS,
890
+ formData || {},
891
+ fallbackBodyForLiquidPreview,
892
+ customValuesForResolve,
893
+ selectedCustomerObj
894
+ );
895
+ const fallbackResponse = await Api.updateEmailPreview(smsFallbackPayload);
896
+ const fallbackPreview = extractPreviewFromLiquidResponse(fallbackResponse);
897
+ setSmsFallbackPreviewText(
898
+ typeof fallbackPreview?.resolvedBody === 'string'
899
+ ? fallbackPreview.resolvedBody
900
+ : undefined
901
+ );
902
+ } catch (e) {
903
+ /* keep existing smsFallbackPreviewText on failure */
904
+ }
905
+ };
906
+
618
907
  /**
619
908
  * Prepare payload for tag extraction based on channel
620
909
  */
@@ -709,7 +998,7 @@ const CommonTestAndPreview = (props) => {
709
998
  } = carousel || {};
710
999
  const buttonData = buttons.map((button, index) => {
711
1000
  const {
712
- type, text, phone_number, urlType, url,
1001
+ type, text, phone_number: phoneNumber, urlType, url,
713
1002
  } = button || {};
714
1003
  const buttonObj = {
715
1004
  type,
@@ -717,7 +1006,7 @@ const CommonTestAndPreview = (props) => {
717
1006
  index,
718
1007
  };
719
1008
  if (type === PHONE_NUMBER) {
720
- buttonObj.phoneNumber = phone_number;
1009
+ buttonObj.phoneNumber = phoneNumber;
721
1010
  }
722
1011
  if (type === URL) {
723
1012
  buttonObj.url = url;
@@ -746,7 +1035,130 @@ const CommonTestAndPreview = (props) => {
746
1035
  };
747
1036
  });
748
1037
 
749
- const prepareTestMessagePayload = (channelType, formDataObj, contentStr, customValuesObj, recipientDetails, previewDataObj, deliverySettingsOverride) => {
1038
+ /**
1039
+ * Build createMessageMeta payload for RCS (test message).
1040
+ * rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
1041
+ * Then rcsDeliverySettings, executionParams, clientName last.
1042
+ */
1043
+ const buildRcsTestMessagePayload = (
1044
+ creativeFormData,
1045
+ _unusedEditorContentString,
1046
+ _customValuesObj,
1047
+ deliverySettingsOverride,
1048
+ basePayload,
1049
+ _rcsTestMetaExtras = {},
1050
+ ) => {
1051
+ const rcsSectionFromForm =
1052
+ creativeFormData?.versions?.base?.content?.RCS ?? creativeFormData?.content?.RCS ?? {};
1053
+ const rcsContentFromForm = rcsSectionFromForm?.rcsContent || {};
1054
+ const smsFallbackFromCreativeForm = rcsSectionFromForm?.smsFallBackContent || {};
1055
+ let rcsCardPayloadList = [];
1056
+ if (Array.isArray(rcsContentFromForm?.cardContent)) {
1057
+ rcsCardPayloadList = rcsContentFromForm.cardContent;
1058
+ } else if (rcsContentFromForm?.cardContent) {
1059
+ rcsCardPayloadList = [rcsContentFromForm.cardContent];
1060
+ }
1061
+ // Raw title/description with template tags; SMS fallback uses tagged template fields (pickFirst…).
1062
+ const cardContentForTestMetaApi = rcsCardPayloadList.map((singleRcsCardPayload) => {
1063
+ const normalizedCardMediaForTestApi = singleRcsCardPayload?.media
1064
+ ? normalizeRcsTestCardMedia(singleRcsCardPayload.media)
1065
+ : undefined;
1066
+ const suggestionsFromCard = Array.isArray(singleRcsCardPayload?.suggestions)
1067
+ ? singleRcsCardPayload.suggestions
1068
+ : [];
1069
+ const suggestionsFormattedForTestMeta = suggestionsFromCard.map((suggestionItem, index) =>
1070
+ mapRcsSuggestionForTestMeta(suggestionItem, index));
1071
+ return {
1072
+ title: singleRcsCardPayload?.title ?? '',
1073
+ description: singleRcsCardPayload?.description ?? '',
1074
+ mediaType: singleRcsCardPayload?.mediaType ?? MEDIA_TYPE_TEXT,
1075
+ ...(normalizedCardMediaForTestApi && { media: normalizedCardMediaForTestApi }),
1076
+ ...(suggestionsFormattedForTestMeta.length > 0 && {
1077
+ suggestions: suggestionsFormattedForTestMeta,
1078
+ }),
1079
+ };
1080
+ });
1081
+ const smsFallbackTaggedTemplateBody = pickFirstSmsFallbackTemplateString(
1082
+ smsFallbackFromCreativeForm,
1083
+ ) || '';
1084
+ const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
1085
+ ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
1086
+ : deliverySettingsOverride?.cdmaSenderId;
1087
+ const deliveryFallbackSmsId =
1088
+ typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
1089
+ const creativeFallbackSmsId =
1090
+ smsFallbackFromCreativeForm?.senderId != null
1091
+ ? String(smsFallbackFromCreativeForm.senderId).trim()
1092
+ : '';
1093
+ const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
1094
+
1095
+ const smsFallBackContent =
1096
+ smsFallbackTaggedTemplateBody.trim() !== ''
1097
+ ? { message: smsFallbackTaggedTemplateBody }
1098
+ : undefined;
1099
+
1100
+ // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
1101
+ const accountIdForMeta =
1102
+ rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
1103
+ ? String(rcsContentFromForm.accountId)
1104
+ : undefined;
1105
+
1106
+ const rcsRichCardContent = {
1107
+ contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
1108
+ cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1109
+ cardSettings: rcsContentFromForm?.cardSettings ?? {
1110
+ cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
1111
+ cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
1112
+ },
1113
+ ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
1114
+ };
1115
+
1116
+ const rcsMessageContent = {
1117
+ channel: CHANNELS.RCS,
1118
+ ...(accountIdForMeta && { accountId: accountIdForMeta }),
1119
+ rcsRichCardContent,
1120
+ ...(smsFallBackContent && { smsFallBackContent }),
1121
+ };
1122
+ const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
1123
+ const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
1124
+ const rcsDeliverySettings = {
1125
+ channelSettings: {
1126
+ channel: CHANNELS.RCS,
1127
+ rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
1128
+ domainId:
1129
+ rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
1130
+ ? Number(rcsDomainId)
1131
+ : (deliverySettingsOverride?.domainId ?? 0),
1132
+ fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
1133
+ },
1134
+ additionalSettings: {
1135
+ useTinyUrl: false,
1136
+ encryptUrl: false,
1137
+ linkTrackingEnabled: false,
1138
+ bypassControlUser: false,
1139
+ userSubscriptionDisabled: false,
1140
+ },
1141
+ };
1142
+ const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
1143
+ return {
1144
+ ...restBase,
1145
+ rcsMessageContent,
1146
+ rcsDeliverySettings,
1147
+ executionParams: {},
1148
+ clientName: baseClientName,
1149
+ };
1150
+ };
1151
+
1152
+ const prepareTestMessagePayload = (
1153
+ channelType,
1154
+ formDataObj,
1155
+ contentStr,
1156
+ customValuesObj,
1157
+ recipientDetails,
1158
+ previewDataObj,
1159
+ deliverySettingsOverride,
1160
+ rcsExtra = {},
1161
+ ) => {
750
1162
  // Base payload structure common to all channels
751
1163
  const basePayload = {
752
1164
  ouId: -1,
@@ -827,12 +1239,12 @@ const CommonTestAndPreview = (props) => {
827
1239
  },
828
1240
  smsDeliverySettings: {
829
1241
  channelSettings: {
1242
+ channel: CHANNELS.SMS,
830
1243
  gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
831
1244
  domainId: deliverySettingsOverride?.domainId ?? null,
832
1245
  domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
833
1246
  targetNdnc: false,
834
1247
  cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
835
- channel: CHANNELS.SMS,
836
1248
  },
837
1249
  additionalSettings: {
838
1250
  useTinyUrl: false,
@@ -976,7 +1388,7 @@ const CommonTestAndPreview = (props) => {
976
1388
  return {
977
1389
  ...basePayload,
978
1390
  whatsappMessageContent: {
979
- messageBody: templateEditorValue || '',
1391
+ messageBody: resolvedMessageBody || templateEditorValue || '',
980
1392
  accountId: formDataObj?.accountId || '',
981
1393
  sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
982
1394
  accountName: formDataObj?.accountName || '',
@@ -1001,16 +1413,7 @@ const CommonTestAndPreview = (props) => {
1001
1413
  }
1002
1414
 
1003
1415
  case CHANNELS.RCS:
1004
- return {
1005
- ...basePayload,
1006
- rcsMessageContent: {
1007
- channel: CHANNELS.RCS,
1008
- messageBody: contentStr,
1009
- rcsType: additionalProps?.rcsType,
1010
- rcsImageSrc: formDataObj?.rcsImageSrc,
1011
- rcsSuggestions: formDataObj?.rcsSuggestions,
1012
- },
1013
- };
1416
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
1014
1417
 
1015
1418
  case CHANNELS.INAPP: {
1016
1419
  // InApp payload structure similar to MobilePush
@@ -2112,6 +2515,10 @@ const CommonTestAndPreview = (props) => {
2112
2515
  formatMessage,
2113
2516
  lastModified: formData?.lastModified,
2114
2517
  updatedByName: formData?.updatedByName,
2518
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2519
+ smsFallbackResolvedText,
2520
+ activePreviewTab,
2521
+ onPreviewTabChange: setActivePreviewTab,
2115
2522
  };
2116
2523
  };
2117
2524
 
@@ -2151,7 +2558,12 @@ const CommonTestAndPreview = (props) => {
2151
2558
  }, [show, beeInstance, currentTab, channel]);
2152
2559
 
2153
2560
  /**
2154
- * Initial data load when slidebox opens
2561
+ * Initial data load when slidebox opens.
2562
+ * EXTRACT TAGS CALL SITES (on open/edit RCS):
2563
+ * 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
2564
+ * 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
2565
+ * 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
2566
+ * The "Process extracted tags" effect only processes API results and must not call extract again.
2155
2567
  */
2156
2568
  useEffect(() => {
2157
2569
  if (show) {
@@ -2236,7 +2648,61 @@ const CommonTestAndPreview = (props) => {
2236
2648
  actions.getTestGroupsRequested();
2237
2649
  }
2238
2650
  }
2239
- }, [show, beeInstance, currentTab, channel]);
2651
+ // getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
2652
+ }, [show, beeInstance, currentTab, channel, getCurrentContent]);
2653
+
2654
+ /**
2655
+ * RCS with SMS fallback: extract tags for fallback SMS content as well
2656
+ * (so we can show a separate "Fallback SMS tags" section in left panel).
2657
+ */
2658
+ useEffect(() => {
2659
+ let cancelled = false;
2660
+
2661
+ if (!show || channel !== CHANNELS.RCS) {
2662
+ return () => {
2663
+ cancelled = true;
2664
+ };
2665
+ }
2666
+
2667
+ if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
2668
+ setSmsFallbackExtractedTags([]);
2669
+ setSmsFallbackRequiredTags([]);
2670
+ setSmsFallbackOptionalTags([]);
2671
+ return () => {
2672
+ cancelled = true;
2673
+ };
2674
+ }
2675
+
2676
+ setIsExtractingSmsFallbackTags(true);
2677
+ (async () => {
2678
+ try {
2679
+ const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
2680
+ const payload = {
2681
+ messageTitle: '',
2682
+ messageBody: fallbackBodyForExtractApi || '',
2683
+ };
2684
+ 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
2685
+ let smsFallbackTagTree = response?.data ?? [];
2686
+ if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
2687
+ if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
2688
+ smsFallbackTagTree = [];
2689
+ } else if (smsFallbackTagTree.length === 0) {
2690
+ smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
2691
+ }
2692
+ if (cancelled) return;
2693
+ setSmsFallbackExtractedTags(smsFallbackTagTree);
2694
+ } catch (e) {
2695
+ if (cancelled) return;
2696
+ setSmsFallbackExtractedTags([]);
2697
+ } finally {
2698
+ if (!cancelled) setIsExtractingSmsFallbackTags(false);
2699
+ }
2700
+ })();
2701
+
2702
+ return () => {
2703
+ cancelled = true;
2704
+ };
2705
+ }, [show, channel, smsFallbackContent]);
2240
2706
 
2241
2707
  /**
2242
2708
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -2288,15 +2754,22 @@ const CommonTestAndPreview = (props) => {
2288
2754
  setSelectedCustomer(null);
2289
2755
  setRequiredTags([]);
2290
2756
  setOptionalTags([]);
2757
+ setSmsFallbackExtractedTags([]);
2758
+ setSmsFallbackRequiredTags([]);
2759
+ setSmsFallbackOptionalTags([]);
2760
+ setIsExtractingSmsFallbackTags(false);
2291
2761
  setCustomValues({});
2292
2762
  setShowJSON(false);
2293
2763
  setTagsExtracted(false);
2294
2764
  setPreviewDevice(DESKTOP);
2765
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2766
+ setSmsFallbackPreviewText(undefined);
2295
2767
  setSelectedTestEntities([]);
2296
2768
  actions.clearPrefilledValues();
2297
2769
  } else {
2298
2770
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
2299
2771
  setPreviewDevice(initialDevice);
2772
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2300
2773
  }
2301
2774
  }, [show, initialDevice]);
2302
2775
 
@@ -2305,79 +2778,10 @@ const CommonTestAndPreview = (props) => {
2305
2778
  */
2306
2779
  useEffect(() => {
2307
2780
  if (previewData) {
2308
- setPreviewDataHtml(previewData);
2781
+ setPreviewDataHtml(toPlainPreviewData(previewData));
2309
2782
  }
2310
2783
  }, [previewData]);
2311
2784
 
2312
- /**
2313
- * Process extracted tags and categorize them
2314
- */
2315
- useEffect(() => {
2316
- // Categorize tags into required and optional
2317
- const required = [];
2318
- const optional = [];
2319
- let hasPersonalizationTags = false;
2320
-
2321
- if (extractedTags?.length > 0) {
2322
- const processTag = (tag, parentPath = '') => {
2323
- const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2324
-
2325
- // Skip unsubscribe tag for input fields
2326
- if (tag?.name === UNSUBSCRIBE_TAG_NAME) {
2327
- return;
2328
- }
2329
-
2330
- hasPersonalizationTags = true;
2331
-
2332
- if (tag?.metaData?.userDriven === false) {
2333
- required.push({
2334
- ...tag,
2335
- fullPath: currentPath,
2336
- });
2337
- } else if (tag?.metaData?.userDriven === true) {
2338
- optional.push({
2339
- ...tag,
2340
- fullPath: currentPath,
2341
- });
2342
- }
2343
-
2344
- if (tag?.children?.length > 0) {
2345
- tag.children.forEach((child) => processTag(child, currentPath));
2346
- }
2347
- };
2348
-
2349
- extractedTags.forEach((tag) => processTag(tag));
2350
-
2351
- if (hasPersonalizationTags) {
2352
- setRequiredTags(required);
2353
- setOptionalTags(optional);
2354
- setTagsExtracted(true); // Mark tags as extracted and processed
2355
-
2356
- // Initialize custom values for required tags
2357
- const initialValues = {};
2358
- required.forEach((tag) => {
2359
- initialValues[tag?.fullPath] = '';
2360
- });
2361
- optional.forEach((tag) => {
2362
- initialValues[tag?.fullPath] = '';
2363
- });
2364
- setCustomValues(initialValues);
2365
- } else {
2366
- // Reset all tag-related state if no personalization tags
2367
- setRequiredTags([]);
2368
- setOptionalTags([]);
2369
- setCustomValues({});
2370
- setTagsExtracted(false);
2371
- }
2372
- } else {
2373
- // Reset all tag-related state if no tags
2374
- setRequiredTags([]);
2375
- setOptionalTags([]);
2376
- setCustomValues({});
2377
- setTagsExtracted(false);
2378
- }
2379
- }, [extractedTags]);
2380
-
2381
2785
  /**
2382
2786
  * Handle customer selection and fetch prefilled values
2383
2787
  */
@@ -2385,17 +2789,15 @@ const CommonTestAndPreview = (props) => {
2385
2789
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2386
2790
  setTagsExtracted(true); // Auto-open custom values editor
2387
2791
 
2388
- // Get all available tags
2389
- const allTags = [...requiredTags, ...optionalTags];
2390
2792
  const requiredTagObj = {};
2391
- requiredTags.forEach((tag) => {
2793
+ allRequiredTags.forEach((tag) => {
2392
2794
  requiredTagObj[tag?.fullPath] = '';
2393
2795
  });
2394
2796
  if (allTags.length > 0) {
2395
2797
  const payload = preparePreviewPayload(
2396
- channel,
2798
+ activeChannelForActions,
2397
2799
  formData || {},
2398
- getCurrentContent,
2800
+ activeContentForActions,
2399
2801
  {
2400
2802
  ...requiredTagObj,
2401
2803
  },
@@ -2404,7 +2806,7 @@ const CommonTestAndPreview = (props) => {
2404
2806
  actions.getPrefilledValuesRequested(payload);
2405
2807
  }
2406
2808
  }
2407
- }, [selectedCustomer]);
2809
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2408
2810
 
2409
2811
  /**
2410
2812
  * Update custom values with prefilled values from API
@@ -2415,7 +2817,7 @@ const CommonTestAndPreview = (props) => {
2415
2817
  if (prefilledValues && selectedCustomer) {
2416
2818
  // Always replace all values with prefilled values
2417
2819
  const updatedValues = {};
2418
- [...requiredTags, ...optionalTags].forEach((tag) => {
2820
+ allTags.forEach((tag) => {
2419
2821
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2420
2822
  });
2421
2823
 
@@ -2423,16 +2825,17 @@ const CommonTestAndPreview = (props) => {
2423
2825
 
2424
2826
  // Update preview with prefilled values (this is a valid preview call trigger)
2425
2827
  const payload = preparePreviewPayload(
2426
- channel,
2828
+ activeChannelForActions,
2427
2829
  formData || {},
2428
- getCurrentContent,
2830
+ activeContentForActions,
2429
2831
  updatedValues,
2430
2832
  selectedCustomer
2431
2833
  );
2432
2834
  actions.updatePreviewRequested(payload);
2433
2835
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2836
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2434
2837
  }
2435
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2838
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2436
2839
 
2437
2840
  /**
2438
2841
  * Map channel constants to display names (lowercase for message)
@@ -2526,11 +2929,7 @@ const CommonTestAndPreview = (props) => {
2526
2929
  setTagsExtracted(true); // Auto-open custom values editor
2527
2930
 
2528
2931
  // Clear any existing values while waiting for prefilled values
2529
- const emptyValues = {};
2530
- [...requiredTags, ...optionalTags].forEach((tag) => {
2531
- emptyValues[tag?.fullPath] = '';
2532
- });
2533
- setCustomValues(emptyValues);
2932
+ setCustomValues(buildEmptyValues());
2534
2933
  };
2535
2934
 
2536
2935
  /**
@@ -2544,11 +2943,7 @@ const CommonTestAndPreview = (props) => {
2544
2943
  actions.clearPreviewErrors();
2545
2944
 
2546
2945
  // Initialize empty values for all tags
2547
- const emptyValues = {};
2548
- [...requiredTags, ...optionalTags].forEach((tag) => {
2549
- emptyValues[tag?.fullPath] = '';
2550
- });
2551
- setCustomValues(emptyValues);
2946
+ setCustomValues(buildEmptyValues());
2552
2947
 
2553
2948
  // Don't make preview call when clearing selection - just reset to raw content
2554
2949
  // Preview will be shown using raw formData/content
@@ -2584,17 +2979,17 @@ const CommonTestAndPreview = (props) => {
2584
2979
  */
2585
2980
  const handleDiscardCustomValues = () => {
2586
2981
  // Initialize empty values for all tags
2587
- const emptyValues = {};
2588
- [...requiredTags, ...optionalTags].forEach((tag) => {
2589
- emptyValues[tag?.fullPath] = '';
2590
- });
2982
+ const emptyValues = buildEmptyValues();
2591
2983
  setCustomValues(emptyValues);
2592
2984
 
2985
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
2986
+ setSmsFallbackPreviewText(undefined);
2987
+
2593
2988
  // Update preview with empty values (this is a valid preview call trigger)
2594
2989
  const payload = preparePreviewPayload(
2595
- channel,
2990
+ activeChannelForActions,
2596
2991
  formData || {},
2597
- getCurrentContent,
2992
+ activeContentForActions,
2598
2993
  emptyValues,
2599
2994
  selectedCustomer
2600
2995
  );
@@ -2609,13 +3004,15 @@ const CommonTestAndPreview = (props) => {
2609
3004
  const handleUpdatePreview = async () => {
2610
3005
  try {
2611
3006
  const payload = preparePreviewPayload(
2612
- channel,
3007
+ activeChannelForActions,
2613
3008
  formData || {},
2614
- getCurrentContent,
3009
+ activeContentForActions,
2615
3010
  customValues,
2616
3011
  selectedCustomer
2617
3012
  );
2618
3013
  await actions.updatePreviewRequested(payload);
3014
+
3015
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2619
3016
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2620
3017
  } catch (error) {
2621
3018
  CapNotification.error({
@@ -2625,25 +3022,115 @@ const CommonTestAndPreview = (props) => {
2625
3022
  };
2626
3023
 
2627
3024
  /**
2628
- * Handle extract tags
3025
+ * Categorize extracted tags into required/optional.
2629
3026
  */
2630
- const handleExtractTags = () => {
2631
- // Get content based on channel
2632
- let contentToExtract = getCurrentContent;
3027
+ const categorizeTags = (tagsTree = []) => {
3028
+ const required = [];
3029
+ const optional = [];
3030
+ let hasPersonalizationTags = false;
3031
+ const processTag = (tag, parentPath = '') => {
3032
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
3033
+
3034
+ // Skip unsubscribe tag for input fields
3035
+ if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
3036
+
3037
+ hasPersonalizationTags = true;
3038
+ const userDriven = tag?.metaData?.userDriven;
3039
+ if (userDriven === true) {
3040
+ optional.push({ ...tag, fullPath: currentPath });
3041
+ } else {
3042
+ // false or missing (SMS/DLT extract often omits metaData) → required for test values
3043
+ required.push({ ...tag, fullPath: currentPath });
3044
+ }
3045
+
3046
+ if (tag?.children?.length > 0) {
3047
+ tag.children.forEach((child) => processTag(child, currentPath));
3048
+ }
3049
+ };
3050
+
3051
+ (tagsTree || []).forEach((tag) => processTag(tag));
3052
+ return { required, optional, hasPersonalizationTags };
3053
+ };
3054
+
3055
+ /**
3056
+ * Apply tag extraction when content comes from RCS + SMS fallback (no API call).
3057
+ */
3058
+ const applyRcsSmsFallbackTagExtraction = () => {
3059
+ const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
3060
+ const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
3061
+ let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
3062
+ ? smsFallbackExtractedTags
3063
+ : buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
3064
+ if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
3065
+ fallbackSmsTagTree = [];
3066
+ }
3067
+ const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
3068
+ setRequiredTags(rcsPrimaryCategorized.required);
3069
+ setOptionalTags(rcsPrimaryCategorized.optional);
3070
+ setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
3071
+ setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
3072
+ setTagsExtracted(
3073
+ rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
3074
+ );
3075
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
3076
+ };
2633
3077
 
3078
+ /**
3079
+ * When extract-tags API returns, map Redux `extractedTags` into required/optional so
3080
+ * CustomValuesEditor shows personalization fields (effect was previously commented out).
3081
+ * RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
3082
+ */
3083
+ useEffect(() => {
3084
+ if (!show) return;
3085
+ const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
3086
+ if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
3087
+ applyRcsSmsFallbackTagExtraction();
3088
+ return;
3089
+ }
3090
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
3091
+ let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
3092
+ ? buildSyntheticSmsMustacheTags(smsEditorBody)
3093
+ : (extractedTags ?? []);
3094
+ if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
3095
+ smsTagSource = [];
3096
+ }
3097
+ const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
3098
+ setRequiredTags(required);
3099
+ setOptionalTags(optional);
3100
+ setTagsExtracted(hasPersonalizationTags);
3101
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
3102
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
3103
+ }, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
3104
+
3105
+ /**
3106
+ * Get content to run tag extraction on (channel-specific).
3107
+ */
3108
+ const getContentForTagExtraction = () => {
3109
+ let contentToExtract = activeContentForActions;
2634
3110
  if (channel === CHANNELS.EMAIL && formData) {
2635
3111
  const currentTabData = formData[currentTab - 1];
2636
3112
  const activeTab = currentTabData?.activeTab;
2637
3113
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2638
3114
  contentToExtract = templateContent || contentToExtract;
2639
3115
  }
3116
+ return contentToExtract;
3117
+ };
2640
3118
 
2641
- // Check for personalization tags (excluding unsubscribe)
3119
+ /**
3120
+ * Handle extract tags
3121
+ */
3122
+ const handleExtractTags = () => {
3123
+ if (channel === CHANNELS.RCS) {
3124
+ applyRcsSmsFallbackTagExtraction();
3125
+ return;
3126
+ }
3127
+
3128
+ const contentToExtract = getContentForTagExtraction();
2642
3129
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2643
3130
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3131
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2644
3132
 
2645
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2646
- // If only unsubscribe tag is present, show noTagsExtracted message
3133
+ if (onlyUnsubscribe) {
2647
3134
  setTagsExtracted(false);
2648
3135
  setRequiredTags([]);
2649
3136
  setOptionalTags([]);
@@ -2651,10 +3138,9 @@ const CommonTestAndPreview = (props) => {
2651
3138
  return;
2652
3139
  }
2653
3140
 
2654
- // Extract tags
2655
3141
  setTagsExtracted(true);
2656
3142
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2657
- channel,
3143
+ activeChannelForActions,
2658
3144
  formData || {},
2659
3145
  contentToExtract
2660
3146
  );
@@ -2716,7 +3202,7 @@ const CommonTestAndPreview = (props) => {
2716
3202
  if (existingTestCustomer) {
2717
3203
  const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2718
3204
  if (entityId != null) {
2719
- const id = 'customer:' + normalizeTestEntityId(entityId);
3205
+ const id = normalizeTestEntityId(entityId);
2720
3206
  setSelectedTestEntities((prev) => (
2721
3207
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2722
3208
  ));
@@ -2753,7 +3239,7 @@ const CommonTestAndPreview = (props) => {
2753
3239
  (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
2754
3240
  );
2755
3241
  if (alreadyInTestListByCustomerId) {
2756
- const id = 'customer:' + normalizeTestEntityId(customerIdFromLookup);
3242
+ const id = normalizeTestEntityId(customerIdFromLookup);
2757
3243
  setSelectedTestEntities((prev) => (
2758
3244
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2759
3245
  ));
@@ -2790,26 +3276,21 @@ const CommonTestAndPreview = (props) => {
2790
3276
  const handleSendTestMessage = () => {
2791
3277
  const allUserIds = [];
2792
3278
  selectedTestEntities.forEach((entityId) => {
2793
- if (String(entityId).startsWith('group:')) {
2794
- const rawId = String(entityId).slice('group:'.length);
2795
- const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, rawId));
2796
- if (group) {
2797
- allUserIds.push(...group.userIds);
2798
- }
3279
+ const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
3280
+ if (group) {
3281
+ allUserIds.push(...group.userIds);
2799
3282
  } else {
2800
- const rawId = String(entityId).startsWith('customer:')
2801
- ? String(entityId).slice('customer:'.length)
2802
- : String(entityId);
2803
- allUserIds.push(Number(rawId));
3283
+ allUserIds.push(entityId);
2804
3284
  }
2805
3285
  });
2806
3286
  const uniqueUserIds = [...new Set(allUserIds)];
2807
3287
 
2808
- const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
3288
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
2809
3289
  ? testPreviewDeliverySettings[channel]
2810
3290
  : null;
2811
3291
 
2812
- // Create initial payload based on channel
3292
+ // createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
3293
+ // Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
2813
3294
  const initialPayload = prepareTestMessagePayload(
2814
3295
  channel,
2815
3296
  formData || content || {},
@@ -2817,7 +3298,8 @@ const CommonTestAndPreview = (props) => {
2817
3298
  customValues,
2818
3299
  uniqueUserIds,
2819
3300
  previewData,
2820
- deliveryOverride
3301
+ deliveryOverride,
3302
+ {},
2821
3303
  );
2822
3304
 
2823
3305
  actions.createMessageMetaRequested(
@@ -2850,11 +3332,10 @@ const CommonTestAndPreview = (props) => {
2850
3332
  // ============================================
2851
3333
  // RENDER HELPER FUNCTIONS
2852
3334
  // ============================================
2853
-
2854
3335
  const renderLeftPanelContent = () => (
2855
3336
  <LeftPanelContent
2856
- isExtractingTags={isExtractingTags}
2857
- extractedTags={extractedTags}
3337
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3338
+ extractedTags={leftPanelExtractedTags}
2858
3339
  selectedCustomer={selectedCustomer}
2859
3340
  handleCustomerSelect={handleCustomerSelect}
2860
3341
  handleSearchCustomer={handleSearchCustomer}
@@ -2872,15 +3353,29 @@ const CommonTestAndPreview = (props) => {
2872
3353
 
2873
3354
  const renderCustomValuesEditor = () => (
2874
3355
  <CustomValuesEditor
2875
- isExtractingTags={isExtractingTags}
3356
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
2876
3357
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
2877
3358
  showJSON={showJSON}
2878
3359
  setShowJSON={setShowJSON}
2879
3360
  customValues={customValues}
2880
3361
  handleJSONTextChange={handleJSONTextChange}
2881
- extractedTags={extractedTags}
2882
- requiredTags={requiredTags}
2883
- optionalTags={optionalTags}
3362
+ sections={[
3363
+ {
3364
+ key: channel,
3365
+ title:
3366
+ channel === CHANNELS.RCS
3367
+ ? messages.rcsTagsSectionTitle
3368
+ : messages[`${channel}TagsSectionTitle`],
3369
+ requiredTags,
3370
+ optionalTags,
3371
+ },
3372
+ {
3373
+ key: PREVIEW_TAB_SMS_FALLBACK,
3374
+ title: channel === CHANNELS.RCS && smsFallbackContent?.templateContent ? messages.smsFallbackTagsSectionTitle : null,
3375
+ requiredTags: smsFallbackRequiredTags,
3376
+ optionalTags: smsFallbackOptionalTags,
3377
+ },
3378
+ ]}
2884
3379
  handleCustomValueChange={handleCustomValueChange}
2885
3380
  handleDiscardCustomValues={handleDiscardCustomValues}
2886
3381
  handleUpdatePreview={handleUpdatePreview}
@@ -2896,18 +3391,13 @@ const CommonTestAndPreview = (props) => {
2896
3391
  }));
2897
3392
  };
2898
3393
 
2899
- /** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
3394
+ /** Trim pasted emails (trailing CR/LF). Allow any input for SMS; valid-only check is in renderAddTestCustomerButton. */
2900
3395
  const handleTestCustomersSearch = useCallback((value) => {
2901
3396
  if (value == null || value === '') {
2902
3397
  setSearchValue('');
2903
3398
  return;
2904
3399
  }
2905
- const raw = String(value).trim();
2906
- if (channel === CHANNELS.SMS) {
2907
- setSearchValue(formatPhoneNumber(raw));
2908
- } else {
2909
- setSearchValue(raw);
2910
- }
3400
+ setSearchValue(String(value).trim());
2911
3401
  }, [channel]);
2912
3402
 
2913
3403
  const renderSendTestMessage = () => (
@@ -2925,12 +3415,13 @@ const CommonTestAndPreview = (props) => {
2925
3415
  renderAddTestCustomerButton={renderAddTestCustomerButton}
2926
3416
  formatMessage={formatMessage}
2927
3417
  deliverySettings={testPreviewDeliverySettings[channel]}
2928
- senderDetailsOptions={senderDetailsByChannel[channel]}
3418
+ senderDetailsByChannel={senderDetailsByChannel}
2929
3419
  wecrmAccounts={wecrmAccounts}
2930
3420
  onSaveDeliverySettings={handleSaveDeliverySettings}
2931
3421
  isLoadingSenderDetails={isLoadingSenderDetails}
2932
3422
  smsTraiDltEnabled={smsTraiDltEnabled}
2933
3423
  registeredSenderIds={registeredSenderIds}
3424
+ isChannelSmsFallbackPreviewEnabled={channel === CHANNELS.RCS && !!smsFallbackContent?.templateContent}
2934
3425
  searchValue={searchValue}
2935
3426
  setSearchValue={handleTestCustomersSearch}
2936
3427
  />
@@ -2944,14 +3435,13 @@ const CommonTestAndPreview = (props) => {
2944
3435
 
2945
3436
  const renderAddTestCustomerButton = () => {
2946
3437
  const raw = (searchValue || '').trim();
2947
- const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
2948
3438
  const showAddButton =
2949
3439
  [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
2950
- (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
3440
+ (channel === CHANNELS.EMAIL ? isValidEmail(raw) : isValidMobile(formatPhoneNumber(raw)));
2951
3441
  if (!showAddButton) return null;
2952
3442
  return (
2953
3443
  <AddTestCustomerButton
2954
- searchValue={value}
3444
+ searchValue={channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw}
2955
3445
  handleAddTestCustomer={handleAddTestCustomer}
2956
3446
  />
2957
3447
  );