@capillarytech/creatives-library 8.0.353-alpha.4 → 8.0.353-alpha.6

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 (125) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  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/CapTagList/index.js +10 -0
  15. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +213 -21
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  22. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -76
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  26. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  27. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  28. package/v2Components/CommonTestAndPreview/index.js +691 -186
  29. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  30. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  31. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  32. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  33. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  36. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  37. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  38. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  40. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  41. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  42. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +36 -26
  43. package/v2Components/FormBuilder/index.js +74 -166
  44. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  45. package/v2Components/SmsFallback/constants.js +73 -0
  46. package/v2Components/SmsFallback/index.js +956 -0
  47. package/v2Components/SmsFallback/index.scss +265 -0
  48. package/v2Components/SmsFallback/messages.js +78 -0
  49. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  50. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  51. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  52. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  55. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  56. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  57. package/v2Components/TemplatePreview/_templatePreview.scss +38 -23
  58. package/v2Components/TemplatePreview/constants.js +2 -0
  59. package/v2Components/TemplatePreview/index.js +143 -31
  60. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  61. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  62. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  63. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  64. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  65. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  66. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  67. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  68. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  69. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  70. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  71. package/v2Containers/CreativesContainer/constants.js +9 -0
  72. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  73. package/v2Containers/CreativesContainer/index.js +346 -163
  74. package/v2Containers/CreativesContainer/index.scss +51 -1
  75. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  77. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  81. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  82. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  83. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  84. package/v2Containers/Email/index.js +1 -1
  85. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  86. package/v2Containers/Rcs/constants.js +119 -10
  87. package/v2Containers/Rcs/index.js +2445 -813
  88. package/v2Containers/Rcs/index.scss +280 -8
  89. package/v2Containers/Rcs/messages.js +34 -3
  90. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  91. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  92. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  93. package/v2Containers/Rcs/tests/index.test.js +152 -121
  94. package/v2Containers/Rcs/tests/mockData.js +38 -0
  95. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  96. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  97. package/v2Containers/Rcs/utils.js +478 -11
  98. package/v2Containers/Sms/Create/index.js +106 -40
  99. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  100. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  101. package/v2Containers/SmsTrai/Create/index.js +9 -4
  102. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  103. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  104. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  105. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  106. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  107. package/v2Containers/SmsWrapper/index.js +37 -8
  108. package/v2Containers/TagList/index.js +6 -0
  109. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  110. package/v2Containers/Templates/_templates.scss +166 -9
  111. package/v2Containers/Templates/actions.js +11 -0
  112. package/v2Containers/Templates/constants.js +2 -0
  113. package/v2Containers/Templates/index.js +120 -52
  114. package/v2Containers/Templates/sagas.js +56 -12
  115. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  116. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  117. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  118. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  119. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  120. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  121. package/v2Containers/TemplatesV2/index.js +86 -23
  122. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  123. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  124. package/v2Containers/Whatsapp/index.js +3 -20
  125. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -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,133 @@ 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
+ // Use the component-level smsFallbackContent prop (has rcsSmsFallbackVarMapped) so DLT
1082
+ // {#var#} slots are converted to {{tagName}} mustache tags before sending to createMessageMeta.
1083
+ const smsFallbackTaggedTemplateBody =
1084
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
1085
+ || pickFirstSmsFallbackTemplateString(smsFallbackFromCreativeForm)
1086
+ || '';
1087
+ const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
1088
+ ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
1089
+ : deliverySettingsOverride?.cdmaSenderId;
1090
+ const deliveryFallbackSmsId =
1091
+ typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
1092
+ const creativeFallbackSmsId =
1093
+ smsFallbackFromCreativeForm?.senderId != null
1094
+ ? String(smsFallbackFromCreativeForm.senderId).trim()
1095
+ : '';
1096
+ const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
1097
+
1098
+ const smsFallBackContent =
1099
+ smsFallbackTaggedTemplateBody.trim() !== ''
1100
+ ? { message: smsFallbackTaggedTemplateBody }
1101
+ : undefined;
1102
+
1103
+ // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
1104
+ const accountIdForMeta =
1105
+ rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
1106
+ ? String(rcsContentFromForm.accountId)
1107
+ : undefined;
1108
+
1109
+ const rcsRichCardContent = {
1110
+ contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
1111
+ cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1112
+ cardSettings: rcsContentFromForm?.cardSettings ?? {
1113
+ cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
1114
+ cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
1115
+ },
1116
+ ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
1117
+ };
1118
+
1119
+ const rcsMessageContent = {
1120
+ channel: CHANNELS.RCS,
1121
+ ...(accountIdForMeta && { accountId: accountIdForMeta }),
1122
+ rcsRichCardContent,
1123
+ ...(smsFallBackContent && { smsFallBackContent }),
1124
+ };
1125
+ const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
1126
+ const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
1127
+ const rcsDeliverySettings = {
1128
+ channelSettings: {
1129
+ channel: CHANNELS.RCS,
1130
+ rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
1131
+ domainId:
1132
+ rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
1133
+ ? Number(rcsDomainId)
1134
+ : (deliverySettingsOverride?.domainId ?? 0),
1135
+ fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
1136
+ },
1137
+ additionalSettings: {
1138
+ useTinyUrl: false,
1139
+ encryptUrl: false,
1140
+ linkTrackingEnabled: false,
1141
+ bypassControlUser: false,
1142
+ userSubscriptionDisabled: false,
1143
+ },
1144
+ };
1145
+ const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
1146
+ return {
1147
+ ...restBase,
1148
+ rcsMessageContent,
1149
+ rcsDeliverySettings,
1150
+ executionParams: {},
1151
+ clientName: baseClientName,
1152
+ };
1153
+ };
1154
+
1155
+ const prepareTestMessagePayload = (
1156
+ channelType,
1157
+ formDataObj,
1158
+ contentStr,
1159
+ customValuesObj,
1160
+ recipientDetails,
1161
+ previewDataObj,
1162
+ deliverySettingsOverride,
1163
+ rcsExtra = {},
1164
+ ) => {
750
1165
  // Base payload structure common to all channels
751
1166
  const basePayload = {
752
1167
  ouId: -1,
@@ -827,12 +1242,12 @@ const CommonTestAndPreview = (props) => {
827
1242
  },
828
1243
  smsDeliverySettings: {
829
1244
  channelSettings: {
1245
+ channel: CHANNELS.SMS,
830
1246
  gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
831
1247
  domainId: deliverySettingsOverride?.domainId ?? null,
832
1248
  domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
833
1249
  targetNdnc: false,
834
1250
  cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
835
- channel: CHANNELS.SMS,
836
1251
  },
837
1252
  additionalSettings: {
838
1253
  useTinyUrl: false,
@@ -976,7 +1391,7 @@ const CommonTestAndPreview = (props) => {
976
1391
  return {
977
1392
  ...basePayload,
978
1393
  whatsappMessageContent: {
979
- messageBody: templateEditorValue || '',
1394
+ messageBody: resolvedMessageBody || templateEditorValue || '',
980
1395
  accountId: formDataObj?.accountId || '',
981
1396
  sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
982
1397
  accountName: formDataObj?.accountName || '',
@@ -1001,16 +1416,7 @@ const CommonTestAndPreview = (props) => {
1001
1416
  }
1002
1417
 
1003
1418
  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
- };
1419
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
1014
1420
 
1015
1421
  case CHANNELS.INAPP: {
1016
1422
  // InApp payload structure similar to MobilePush
@@ -2112,6 +2518,10 @@ const CommonTestAndPreview = (props) => {
2112
2518
  formatMessage,
2113
2519
  lastModified: formData?.lastModified,
2114
2520
  updatedByName: formData?.updatedByName,
2521
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2522
+ smsFallbackResolvedText,
2523
+ activePreviewTab,
2524
+ onPreviewTabChange: setActivePreviewTab,
2115
2525
  };
2116
2526
  };
2117
2527
 
@@ -2151,7 +2561,12 @@ const CommonTestAndPreview = (props) => {
2151
2561
  }, [show, beeInstance, currentTab, channel]);
2152
2562
 
2153
2563
  /**
2154
- * Initial data load when slidebox opens
2564
+ * Initial data load when slidebox opens.
2565
+ * EXTRACT TAGS CALL SITES (on open/edit RCS):
2566
+ * 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
2567
+ * 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
2568
+ * 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
2569
+ * The "Process extracted tags" effect only processes API results and must not call extract again.
2155
2570
  */
2156
2571
  useEffect(() => {
2157
2572
  if (show) {
@@ -2236,7 +2651,61 @@ const CommonTestAndPreview = (props) => {
2236
2651
  actions.getTestGroupsRequested();
2237
2652
  }
2238
2653
  }
2239
- }, [show, beeInstance, currentTab, channel]);
2654
+ // getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
2655
+ }, [show, beeInstance, currentTab, channel, getCurrentContent]);
2656
+
2657
+ /**
2658
+ * RCS with SMS fallback: extract tags for fallback SMS content as well
2659
+ * (so we can show a separate "Fallback SMS tags" section in left panel).
2660
+ */
2661
+ useEffect(() => {
2662
+ let cancelled = false;
2663
+
2664
+ if (!show || channel !== CHANNELS.RCS) {
2665
+ return () => {
2666
+ cancelled = true;
2667
+ };
2668
+ }
2669
+
2670
+ if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
2671
+ setSmsFallbackExtractedTags([]);
2672
+ setSmsFallbackRequiredTags([]);
2673
+ setSmsFallbackOptionalTags([]);
2674
+ return () => {
2675
+ cancelled = true;
2676
+ };
2677
+ }
2678
+
2679
+ setIsExtractingSmsFallbackTags(true);
2680
+ (async () => {
2681
+ try {
2682
+ const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
2683
+ const payload = {
2684
+ messageTitle: '',
2685
+ messageBody: fallbackBodyForExtractApi || '',
2686
+ };
2687
+ 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
2688
+ let smsFallbackTagTree = response?.data ?? [];
2689
+ if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
2690
+ if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
2691
+ smsFallbackTagTree = [];
2692
+ } else if (smsFallbackTagTree.length === 0) {
2693
+ smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
2694
+ }
2695
+ if (cancelled) return;
2696
+ setSmsFallbackExtractedTags(smsFallbackTagTree);
2697
+ } catch (e) {
2698
+ if (cancelled) return;
2699
+ setSmsFallbackExtractedTags([]);
2700
+ } finally {
2701
+ if (!cancelled) setIsExtractingSmsFallbackTags(false);
2702
+ }
2703
+ })();
2704
+
2705
+ return () => {
2706
+ cancelled = true;
2707
+ };
2708
+ }, [show, channel, smsFallbackContent]);
2240
2709
 
2241
2710
  /**
2242
2711
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -2288,15 +2757,22 @@ const CommonTestAndPreview = (props) => {
2288
2757
  setSelectedCustomer(null);
2289
2758
  setRequiredTags([]);
2290
2759
  setOptionalTags([]);
2760
+ setSmsFallbackExtractedTags([]);
2761
+ setSmsFallbackRequiredTags([]);
2762
+ setSmsFallbackOptionalTags([]);
2763
+ setIsExtractingSmsFallbackTags(false);
2291
2764
  setCustomValues({});
2292
2765
  setShowJSON(false);
2293
2766
  setTagsExtracted(false);
2294
2767
  setPreviewDevice(DESKTOP);
2768
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2769
+ setSmsFallbackPreviewText(undefined);
2295
2770
  setSelectedTestEntities([]);
2296
2771
  actions.clearPrefilledValues();
2297
2772
  } else {
2298
2773
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
2299
2774
  setPreviewDevice(initialDevice);
2775
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2300
2776
  }
2301
2777
  }, [show, initialDevice]);
2302
2778
 
@@ -2305,79 +2781,10 @@ const CommonTestAndPreview = (props) => {
2305
2781
  */
2306
2782
  useEffect(() => {
2307
2783
  if (previewData) {
2308
- setPreviewDataHtml(previewData);
2784
+ setPreviewDataHtml(toPlainPreviewData(previewData));
2309
2785
  }
2310
2786
  }, [previewData]);
2311
2787
 
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
2788
  /**
2382
2789
  * Handle customer selection and fetch prefilled values
2383
2790
  */
@@ -2385,17 +2792,15 @@ const CommonTestAndPreview = (props) => {
2385
2792
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2386
2793
  setTagsExtracted(true); // Auto-open custom values editor
2387
2794
 
2388
- // Get all available tags
2389
- const allTags = [...requiredTags, ...optionalTags];
2390
2795
  const requiredTagObj = {};
2391
- requiredTags.forEach((tag) => {
2796
+ allRequiredTags.forEach((tag) => {
2392
2797
  requiredTagObj[tag?.fullPath] = '';
2393
2798
  });
2394
2799
  if (allTags.length > 0) {
2395
2800
  const payload = preparePreviewPayload(
2396
- channel,
2801
+ activeChannelForActions,
2397
2802
  formData || {},
2398
- getCurrentContent,
2803
+ activeContentForActions,
2399
2804
  {
2400
2805
  ...requiredTagObj,
2401
2806
  },
@@ -2404,7 +2809,7 @@ const CommonTestAndPreview = (props) => {
2404
2809
  actions.getPrefilledValuesRequested(payload);
2405
2810
  }
2406
2811
  }
2407
- }, [selectedCustomer]);
2812
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2408
2813
 
2409
2814
  /**
2410
2815
  * Update custom values with prefilled values from API
@@ -2415,24 +2820,29 @@ const CommonTestAndPreview = (props) => {
2415
2820
  if (prefilledValues && selectedCustomer) {
2416
2821
  // Always replace all values with prefilled values
2417
2822
  const updatedValues = {};
2418
- [...requiredTags, ...optionalTags].forEach((tag) => {
2823
+ allTags.forEach((tag) => {
2419
2824
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2420
2825
  });
2421
2826
 
2422
2827
  setCustomValues(updatedValues);
2423
2828
 
2424
2829
  // Update preview with prefilled values (this is a valid preview call trigger)
2830
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
2831
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
2832
+ const previewChannelForPrefill = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
2833
+ const previewContentForPrefill = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2425
2834
  const payload = preparePreviewPayload(
2426
- channel,
2835
+ previewChannelForPrefill,
2427
2836
  formData || {},
2428
- getCurrentContent,
2837
+ previewContentForPrefill,
2429
2838
  updatedValues,
2430
2839
  selectedCustomer
2431
2840
  );
2432
2841
  actions.updatePreviewRequested(payload);
2433
2842
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2843
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2434
2844
  }
2435
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2845
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2436
2846
 
2437
2847
  /**
2438
2848
  * Map channel constants to display names (lowercase for message)
@@ -2526,11 +2936,7 @@ const CommonTestAndPreview = (props) => {
2526
2936
  setTagsExtracted(true); // Auto-open custom values editor
2527
2937
 
2528
2938
  // 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);
2939
+ setCustomValues(buildEmptyValues());
2534
2940
  };
2535
2941
 
2536
2942
  /**
@@ -2544,11 +2950,7 @@ const CommonTestAndPreview = (props) => {
2544
2950
  actions.clearPreviewErrors();
2545
2951
 
2546
2952
  // Initialize empty values for all tags
2547
- const emptyValues = {};
2548
- [...requiredTags, ...optionalTags].forEach((tag) => {
2549
- emptyValues[tag?.fullPath] = '';
2550
- });
2551
- setCustomValues(emptyValues);
2953
+ setCustomValues(buildEmptyValues());
2552
2954
 
2553
2955
  // Don't make preview call when clearing selection - just reset to raw content
2554
2956
  // Preview will be shown using raw formData/content
@@ -2584,17 +2986,20 @@ const CommonTestAndPreview = (props) => {
2584
2986
  */
2585
2987
  const handleDiscardCustomValues = () => {
2586
2988
  // Initialize empty values for all tags
2587
- const emptyValues = {};
2588
- [...requiredTags, ...optionalTags].forEach((tag) => {
2589
- emptyValues[tag?.fullPath] = '';
2590
- });
2989
+ const emptyValues = buildEmptyValues();
2591
2990
  setCustomValues(emptyValues);
2592
2991
 
2992
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
2993
+ setSmsFallbackPreviewText(undefined);
2994
+
2593
2995
  // Update preview with empty values (this is a valid preview call trigger)
2996
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
2997
+ const previewChannelForDiscard = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
2998
+ const previewContentForDiscard = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2594
2999
  const payload = preparePreviewPayload(
2595
- channel,
3000
+ previewChannelForDiscard,
2596
3001
  formData || {},
2597
- getCurrentContent,
3002
+ previewContentForDiscard,
2598
3003
  emptyValues,
2599
3004
  selectedCustomer
2600
3005
  );
@@ -2608,14 +3013,21 @@ const CommonTestAndPreview = (props) => {
2608
3013
  */
2609
3014
  const handleUpdatePreview = async () => {
2610
3015
  try {
3016
+ // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped,
3017
+ // even when the user triggers update from the SMS fallback tab.
3018
+ // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
3019
+ const previewChannel = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
3020
+ const previewContent = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
2611
3021
  const payload = preparePreviewPayload(
2612
- channel,
3022
+ previewChannel,
2613
3023
  formData || {},
2614
- getCurrentContent,
3024
+ previewContent,
2615
3025
  customValues,
2616
3026
  selectedCustomer
2617
3027
  );
2618
3028
  await actions.updatePreviewRequested(payload);
3029
+
3030
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2619
3031
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2620
3032
  } catch (error) {
2621
3033
  CapNotification.error({
@@ -2625,25 +3037,115 @@ const CommonTestAndPreview = (props) => {
2625
3037
  };
2626
3038
 
2627
3039
  /**
2628
- * Handle extract tags
3040
+ * Categorize extracted tags into required/optional.
2629
3041
  */
2630
- const handleExtractTags = () => {
2631
- // Get content based on channel
2632
- let contentToExtract = getCurrentContent;
3042
+ const categorizeTags = (tagsTree = []) => {
3043
+ const required = [];
3044
+ const optional = [];
3045
+ let hasPersonalizationTags = false;
3046
+ const processTag = (tag, parentPath = '') => {
3047
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
3048
+
3049
+ // Skip unsubscribe tag for input fields
3050
+ if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
3051
+
3052
+ hasPersonalizationTags = true;
3053
+ const userDriven = tag?.metaData?.userDriven;
3054
+ if (userDriven === true) {
3055
+ optional.push({ ...tag, fullPath: currentPath });
3056
+ } else {
3057
+ // false or missing (SMS/DLT extract often omits metaData) → required for test values
3058
+ required.push({ ...tag, fullPath: currentPath });
3059
+ }
3060
+
3061
+ if (tag?.children?.length > 0) {
3062
+ tag.children.forEach((child) => processTag(child, currentPath));
3063
+ }
3064
+ };
3065
+
3066
+ (tagsTree || []).forEach((tag) => processTag(tag));
3067
+ return { required, optional, hasPersonalizationTags };
3068
+ };
3069
+
3070
+ /**
3071
+ * Apply tag extraction when content comes from RCS + SMS fallback (no API call).
3072
+ */
3073
+ const applyRcsSmsFallbackTagExtraction = () => {
3074
+ const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
3075
+ const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
3076
+ let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
3077
+ ? smsFallbackExtractedTags
3078
+ : buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
3079
+ if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
3080
+ fallbackSmsTagTree = [];
3081
+ }
3082
+ const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
3083
+ setRequiredTags(rcsPrimaryCategorized.required);
3084
+ setOptionalTags(rcsPrimaryCategorized.optional);
3085
+ setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
3086
+ setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
3087
+ setTagsExtracted(
3088
+ rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
3089
+ );
3090
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
3091
+ };
2633
3092
 
3093
+ /**
3094
+ * When extract-tags API returns, map Redux `extractedTags` into required/optional so
3095
+ * CustomValuesEditor shows personalization fields (effect was previously commented out).
3096
+ * RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
3097
+ */
3098
+ useEffect(() => {
3099
+ if (!show) return;
3100
+ const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
3101
+ if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
3102
+ applyRcsSmsFallbackTagExtraction();
3103
+ return;
3104
+ }
3105
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
3106
+ let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
3107
+ ? buildSyntheticSmsMustacheTags(smsEditorBody)
3108
+ : (extractedTags ?? []);
3109
+ if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
3110
+ smsTagSource = [];
3111
+ }
3112
+ const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
3113
+ setRequiredTags(required);
3114
+ setOptionalTags(optional);
3115
+ setTagsExtracted(hasPersonalizationTags);
3116
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
3117
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
3118
+ }, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
3119
+
3120
+ /**
3121
+ * Get content to run tag extraction on (channel-specific).
3122
+ */
3123
+ const getContentForTagExtraction = () => {
3124
+ let contentToExtract = activeContentForActions;
2634
3125
  if (channel === CHANNELS.EMAIL && formData) {
2635
3126
  const currentTabData = formData[currentTab - 1];
2636
3127
  const activeTab = currentTabData?.activeTab;
2637
3128
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2638
3129
  contentToExtract = templateContent || contentToExtract;
2639
3130
  }
3131
+ return contentToExtract;
3132
+ };
2640
3133
 
2641
- // Check for personalization tags (excluding unsubscribe)
3134
+ /**
3135
+ * Handle extract tags
3136
+ */
3137
+ const handleExtractTags = () => {
3138
+ if (channel === CHANNELS.RCS) {
3139
+ applyRcsSmsFallbackTagExtraction();
3140
+ return;
3141
+ }
3142
+
3143
+ const contentToExtract = getContentForTagExtraction();
2642
3144
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2643
3145
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3146
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2644
3147
 
2645
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2646
- // If only unsubscribe tag is present, show noTagsExtracted message
3148
+ if (onlyUnsubscribe) {
2647
3149
  setTagsExtracted(false);
2648
3150
  setRequiredTags([]);
2649
3151
  setOptionalTags([]);
@@ -2651,10 +3153,9 @@ const CommonTestAndPreview = (props) => {
2651
3153
  return;
2652
3154
  }
2653
3155
 
2654
- // Extract tags
2655
3156
  setTagsExtracted(true);
2656
3157
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2657
- channel,
3158
+ activeChannelForActions,
2658
3159
  formData || {},
2659
3160
  contentToExtract
2660
3161
  );
@@ -2716,7 +3217,7 @@ const CommonTestAndPreview = (props) => {
2716
3217
  if (existingTestCustomer) {
2717
3218
  const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2718
3219
  if (entityId != null) {
2719
- const id = 'customer:' + normalizeTestEntityId(entityId);
3220
+ const id = normalizeTestEntityId(entityId);
2720
3221
  setSelectedTestEntities((prev) => (
2721
3222
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2722
3223
  ));
@@ -2753,7 +3254,7 @@ const CommonTestAndPreview = (props) => {
2753
3254
  (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
2754
3255
  );
2755
3256
  if (alreadyInTestListByCustomerId) {
2756
- const id = 'customer:' + normalizeTestEntityId(customerIdFromLookup);
3257
+ const id = normalizeTestEntityId(customerIdFromLookup);
2757
3258
  setSelectedTestEntities((prev) => (
2758
3259
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2759
3260
  ));
@@ -2790,26 +3291,21 @@ const CommonTestAndPreview = (props) => {
2790
3291
  const handleSendTestMessage = () => {
2791
3292
  const allUserIds = [];
2792
3293
  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
- }
3294
+ const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
3295
+ if (group) {
3296
+ allUserIds.push(...group.userIds);
2799
3297
  } else {
2800
- const rawId = String(entityId).startsWith('customer:')
2801
- ? String(entityId).slice('customer:'.length)
2802
- : String(entityId);
2803
- allUserIds.push(Number(rawId));
3298
+ allUserIds.push(entityId);
2804
3299
  }
2805
3300
  });
2806
3301
  const uniqueUserIds = [...new Set(allUserIds)];
2807
3302
 
2808
- const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
3303
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
2809
3304
  ? testPreviewDeliverySettings[channel]
2810
3305
  : null;
2811
3306
 
2812
- // Create initial payload based on channel
3307
+ // createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
3308
+ // Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
2813
3309
  const initialPayload = prepareTestMessagePayload(
2814
3310
  channel,
2815
3311
  formData || content || {},
@@ -2817,7 +3313,8 @@ const CommonTestAndPreview = (props) => {
2817
3313
  customValues,
2818
3314
  uniqueUserIds,
2819
3315
  previewData,
2820
- deliveryOverride
3316
+ deliveryOverride,
3317
+ {},
2821
3318
  );
2822
3319
 
2823
3320
  actions.createMessageMetaRequested(
@@ -2850,11 +3347,10 @@ const CommonTestAndPreview = (props) => {
2850
3347
  // ============================================
2851
3348
  // RENDER HELPER FUNCTIONS
2852
3349
  // ============================================
2853
-
2854
3350
  const renderLeftPanelContent = () => (
2855
3351
  <LeftPanelContent
2856
- isExtractingTags={isExtractingTags}
2857
- extractedTags={extractedTags}
3352
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3353
+ extractedTags={leftPanelExtractedTags}
2858
3354
  selectedCustomer={selectedCustomer}
2859
3355
  handleCustomerSelect={handleCustomerSelect}
2860
3356
  handleSearchCustomer={handleSearchCustomer}
@@ -2872,15 +3368,29 @@ const CommonTestAndPreview = (props) => {
2872
3368
 
2873
3369
  const renderCustomValuesEditor = () => (
2874
3370
  <CustomValuesEditor
2875
- isExtractingTags={isExtractingTags}
3371
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
2876
3372
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
2877
3373
  showJSON={showJSON}
2878
3374
  setShowJSON={setShowJSON}
2879
3375
  customValues={customValues}
2880
3376
  handleJSONTextChange={handleJSONTextChange}
2881
- extractedTags={extractedTags}
2882
- requiredTags={requiredTags}
2883
- optionalTags={optionalTags}
3377
+ sections={[
3378
+ {
3379
+ key: channel,
3380
+ title:
3381
+ channel === CHANNELS.RCS
3382
+ ? messages.rcsTagsSectionTitle
3383
+ : messages[`${channel}TagsSectionTitle`],
3384
+ requiredTags,
3385
+ optionalTags,
3386
+ },
3387
+ {
3388
+ key: PREVIEW_TAB_SMS_FALLBACK,
3389
+ title: isRcsSmsFallbackPreviewEnabled ? messages.smsFallbackTagsSectionTitle : null,
3390
+ requiredTags: smsFallbackRequiredTags,
3391
+ optionalTags: smsFallbackOptionalTags,
3392
+ },
3393
+ ]}
2884
3394
  handleCustomValueChange={handleCustomValueChange}
2885
3395
  handleDiscardCustomValues={handleDiscardCustomValues}
2886
3396
  handleUpdatePreview={handleUpdatePreview}
@@ -2896,18 +3406,13 @@ const CommonTestAndPreview = (props) => {
2896
3406
  }));
2897
3407
  };
2898
3408
 
2899
- /** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
3409
+ /** Trim pasted emails (trailing CR/LF). Allow any input for SMS; valid-only check is in renderAddTestCustomerButton. */
2900
3410
  const handleTestCustomersSearch = useCallback((value) => {
2901
3411
  if (value == null || value === '') {
2902
3412
  setSearchValue('');
2903
3413
  return;
2904
3414
  }
2905
- const raw = String(value).trim();
2906
- if (channel === CHANNELS.SMS) {
2907
- setSearchValue(formatPhoneNumber(raw));
2908
- } else {
2909
- setSearchValue(raw);
2910
- }
3415
+ setSearchValue(String(value).trim());
2911
3416
  }, [channel]);
2912
3417
 
2913
3418
  const renderSendTestMessage = () => (
@@ -2925,12 +3430,13 @@ const CommonTestAndPreview = (props) => {
2925
3430
  renderAddTestCustomerButton={renderAddTestCustomerButton}
2926
3431
  formatMessage={formatMessage}
2927
3432
  deliverySettings={testPreviewDeliverySettings[channel]}
2928
- senderDetailsOptions={senderDetailsByChannel[channel]}
3433
+ senderDetailsByChannel={senderDetailsByChannel}
2929
3434
  wecrmAccounts={wecrmAccounts}
2930
3435
  onSaveDeliverySettings={handleSaveDeliverySettings}
2931
3436
  isLoadingSenderDetails={isLoadingSenderDetails}
2932
3437
  smsTraiDltEnabled={smsTraiDltEnabled}
2933
3438
  registeredSenderIds={registeredSenderIds}
3439
+ isChannelSmsFallbackPreviewEnabled={isRcsSmsFallbackPreviewEnabled}
2934
3440
  searchValue={searchValue}
2935
3441
  setSearchValue={handleTestCustomersSearch}
2936
3442
  />
@@ -2944,14 +3450,13 @@ const CommonTestAndPreview = (props) => {
2944
3450
 
2945
3451
  const renderAddTestCustomerButton = () => {
2946
3452
  const raw = (searchValue || '').trim();
2947
- const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
2948
3453
  const showAddButton =
2949
3454
  [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
2950
- (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
3455
+ (channel === CHANNELS.EMAIL ? isValidEmail(raw) : isValidMobile(formatPhoneNumber(raw)));
2951
3456
  if (!showAddButton) return null;
2952
3457
  return (
2953
3458
  <AddTestCustomerButton
2954
- searchValue={value}
3459
+ searchValue={channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw}
2955
3460
  handleAddTestCustomer={handleAddTestCustomer}
2956
3461
  />
2957
3462
  );