@capillarytech/creatives-library 8.0.353-alpha.5 → 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 (136) 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/PreviewHeader.js +0 -17
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -146
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +138 -48
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -4
  29. package/v2Components/CommonTestAndPreview/index.js +691 -235
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +0 -159
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -256
  42. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -2
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -198
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +36 -26
  46. package/v2Components/FormBuilder/index.js +11 -6
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +956 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -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 +223 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -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 +38 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -31
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +15 -3
  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/App/constants.js +0 -3
  71. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  72. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  73. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  74. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  75. package/v2Containers/CreativesContainer/constants.js +9 -0
  76. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  77. package/v2Containers/CreativesContainer/index.js +322 -103
  78. package/v2Containers/CreativesContainer/index.scss +51 -1
  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 -15
  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/MobilePush/Create/test/saga.test.js +2 -2
  89. package/v2Containers/Rcs/constants.js +119 -10
  90. package/v2Containers/Rcs/index.js +2445 -813
  91. package/v2Containers/Rcs/index.scss +280 -8
  92. package/v2Containers/Rcs/messages.js +34 -3
  93. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  94. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  95. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  96. package/v2Containers/Rcs/tests/index.test.js +152 -121
  97. package/v2Containers/Rcs/tests/mockData.js +38 -0
  98. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  99. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  100. package/v2Containers/Rcs/utils.js +478 -11
  101. package/v2Containers/Sms/Create/index.js +106 -40
  102. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  103. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  104. package/v2Containers/SmsTrai/Create/index.js +9 -4
  105. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  106. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  107. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  108. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  109. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  110. package/v2Containers/SmsWrapper/index.js +37 -8
  111. package/v2Containers/TagList/index.js +6 -0
  112. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  113. package/v2Containers/Templates/_templates.scss +166 -9
  114. package/v2Containers/Templates/actions.js +11 -0
  115. package/v2Containers/Templates/constants.js +2 -0
  116. package/v2Containers/Templates/index.js +122 -120
  117. package/v2Containers/Templates/sagas.js +56 -12
  118. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  119. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  120. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  121. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  122. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  123. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  124. package/v2Containers/TemplatesV2/index.js +86 -23
  125. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  126. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  127. package/v2Containers/WebPush/Create/index.js +8 -91
  128. package/v2Containers/WebPush/Create/index.scss +0 -7
  129. package/v2Containers/Whatsapp/index.js +3 -20
  130. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  131. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +0 -169
  132. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +0 -522
  133. package/v2Containers/App/tests/constants.test.js +0 -61
  134. package/v2Containers/Templates/tests/webpush.test.js +0 -375
  135. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +0 -338
  136. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +0 -325
@@ -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,12 +82,23 @@ import {
81
82
  IMAGE,
82
83
  VIDEO,
83
84
  URL,
85
+ PREVIEW_TAB_RCS,
86
+ PREVIEW_TAB_SMS_FALLBACK,
84
87
  CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
85
- DAYS,
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,
86
93
  } from './constants';
87
-
88
- // Import utilities
89
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
+
90
102
  import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
91
103
  import { getMembersLookup } from '../../services/api';
92
104
 
@@ -109,6 +121,85 @@ const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEch
109
121
  });
110
122
  };
111
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
+ });
112
203
 
113
204
  /**
114
205
  * CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
@@ -134,7 +225,7 @@ const testEntityIdsEqual = (a, b) => {
134
225
  */
135
226
  const CommonTestAndPreview = (props) => {
136
227
  const {
137
- intl: { formatMessage },
228
+ intl: { formatMessage, locale: userLocale = 'en' },
138
229
  show,
139
230
  onClose,
140
231
  channel, // The channel: 'EMAIL', 'SMS', 'RCS', etc.
@@ -170,12 +261,21 @@ const CommonTestAndPreview = (props) => {
170
261
  ...additionalProps
171
262
  } = props;
172
263
 
264
+ const smsFallbackContent = additionalProps?.smsFallbackContent;
265
+ const smsFallbackTextForTagExtraction = useMemo(
266
+ () => getSmsFallbackTextForTagExtraction(smsFallbackContent),
267
+ [smsFallbackContent],
268
+ );
173
269
  // ============================================
174
270
  // STATE MANAGEMENT
175
271
  // ============================================
176
272
  const [selectedCustomer, setSelectedCustomer] = useState(null);
177
273
  const [requiredTags, setRequiredTags] = useState([]);
178
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);
179
279
  const [customValues, setCustomValues] = useState({});
180
280
  const [showJSON, setShowJSON] = useState(false);
181
281
  const [tagsExtracted, setTagsExtracted] = useState(false);
@@ -187,6 +287,8 @@ const CommonTestAndPreview = (props) => {
187
287
  const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
188
288
 
189
289
  const [previewDevice, setPreviewDevice] = useState(initialDevice);
290
+ const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
291
+ const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
190
292
  // Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
191
293
  const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
192
294
  const [previewDataHtml, setPreviewDataHtml] = useState(() => {
@@ -219,15 +321,22 @@ const CommonTestAndPreview = (props) => {
219
321
  [CHANNELS.WHATSAPP]: {
220
322
  domainId: null, senderMobNum: '', sourceAccountIdentifier: '',
221
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
+ },
222
331
  });
223
332
 
224
- const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP];
333
+ const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
225
334
  const formDataForSendTest = formData ?? (content && typeof content === 'object' && !Array.isArray(content) ? content : formData);
226
335
  const smsTemplateConfigs = formDataForSendTest?.templateConfigs || {};
227
336
  const smsTraiDltEnabled = !!smsTemplateConfigs?.traiDltEnabled;
228
337
  const registeredSenderIds = smsTemplateConfigs?.registeredSenderIds || [];
229
338
 
230
- // 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)
231
340
  useEffect(() => {
232
341
  if (!show || !channel) {
233
342
  return;
@@ -236,39 +345,99 @@ const CommonTestAndPreview = (props) => {
236
345
  if (actions.getSenderDetailsRequested) {
237
346
  actions.getSenderDetailsRequested({ channel, orgUnitId: orgUnitId ?? -1 });
238
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
+ }
239
352
  if (channel === CHANNELS.WHATSAPP && actions.getWeCrmAccountsRequested) {
240
353
  actions.getWeCrmAccountsRequested({ sourceName: CHANNELS.WHATSAPP });
241
354
  }
242
355
  }
243
356
  }, [show, channel, orgUnitId, actions]);
244
357
 
245
- useEffect(() => {
246
- if (!show) {
247
- setCustomerModal([false, '']);
248
- setSearchValue('');
249
- setCustomerData({ name: '', email: '', mobile: '', customerId: '' });
250
- }
251
- }, [show]);
252
-
253
358
  const findDefault = (arr) => (arr && arr.find((x) => x.default)) || (arr && arr[0]) || {};
254
359
 
255
360
  // Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
256
361
  useEffect(() => {
257
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
+
258
426
  const domains = senderDetailsByChannel[channel];
259
427
  if (!domains || domains.length === 0) return;
260
428
  const {
261
- domainId = '', gsmSenderId = '', senderEmail = '', senderMobNum = '',
429
+ domainId = '', gsmSenderId = '', cdmaSenderId = '', senderEmail = '', senderMobNum = '',
262
430
  } = testPreviewDeliverySettings[channel] || {};
263
- const isEmptySelection = !domainId && !gsmSenderId && !senderEmail && !senderMobNum;
431
+ const isEmptySelection = !domainId && !gsmSenderId && !cdmaSenderId && !senderEmail && !senderMobNum;
264
432
  if (!isEmptySelection) return;
265
433
 
266
434
  const whatsappAccountFromForm = channel === CHANNELS.WHATSAPP ? formData?.accountName : undefined;
267
435
  const matchedWhatsappAccount = whatsappAccountFromForm
268
436
  ? (wecrmAccounts || []).find((account) => account?.name === whatsappAccountFromForm)
269
437
  : null;
270
- const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled
271
- ? 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)))
272
441
  : domains;
273
442
  const [defaultDomain] = domains;
274
443
  const [firstSmsDomain] = smsDomains;
@@ -283,8 +452,8 @@ const CommonTestAndPreview = (props) => {
283
452
  const next = { ...prev };
284
453
  if (channel === CHANNELS.SMS) {
285
454
  const smsGsmSenders = smsTraiDltEnabled
286
- ? (firstDomain.gsmSenders || []).filter((gsm) => registeredSenderIds.includes(gsm.value))
287
- : firstDomain.gsmSenders;
455
+ ? (firstDomain?.gsmSenders || []).filter((gsm) => registeredSenderIds?.includes(gsm?.value))
456
+ : firstDomain?.gsmSenders;
288
457
  next[channel] = {
289
458
  domainId: firstDomain.domainId,
290
459
  domainGatewayMapId: firstDomain.dgmId,
@@ -317,10 +486,29 @@ const CommonTestAndPreview = (props) => {
317
486
  // MEMOIZED VALUES
318
487
  // ============================================
319
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
+
320
508
  // Check if update preview button should be disabled
321
509
  const isUpdatePreviewDisabled = useMemo(() => (
322
- requiredTags.some((tag) => !customValues[tag.fullPath])
323
- ), [requiredTags, customValues]);
510
+ allRequiredTags.some((tag) => !customValues[tag.fullPath])
511
+ ), [allRequiredTags, customValues]);
324
512
 
325
513
  // Get current content based on channel and editor type
326
514
  const getCurrentContent = useMemo(() => {
@@ -364,6 +552,13 @@ const CommonTestAndPreview = (props) => {
364
552
  return currentTabData.base['sms-editor'];
365
553
  }
366
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
+ }
367
562
  }
368
563
 
369
564
  // SMS channel fallback - if formData is not available, use content directly
@@ -409,7 +604,70 @@ const CommonTestAndPreview = (props) => {
409
604
  return content || '';
410
605
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
411
606
 
412
- // 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
413
671
  // Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
414
672
  const testEntitiesTreeData = useMemo(() => {
415
673
  const groupsNode = {
@@ -418,7 +676,7 @@ const CommonTestAndPreview = (props) => {
418
676
  selectable: false,
419
677
  children: testGroups?.map((group) => ({
420
678
  title: group?.groupName,
421
- value: 'group:' + normalizeTestEntityId(group?.groupId),
679
+ value: normalizeTestEntityId(group?.groupId),
422
680
  })),
423
681
  };
424
682
 
@@ -428,7 +686,7 @@ const CommonTestAndPreview = (props) => {
428
686
  selectable: false,
429
687
  children: testCustomers?.map((customer) => ({
430
688
  title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
431
- value: 'customer:' + normalizeTestEntityId(customer?.userId ?? customer?.customerId),
689
+ value: normalizeTestEntityId(customer?.userId ?? customer?.customerId),
432
690
  })) || [],
433
691
  };
434
692
 
@@ -508,11 +766,10 @@ const CommonTestAndPreview = (props) => {
508
766
  email: customerData?.email || '',
509
767
  mobile: customerData?.mobile || '',
510
768
  });
511
- const prefixedAddedId = 'customer:' + normalizedAddedId;
512
769
  setSelectedTestEntities((prev) => (
513
- prev.some((id) => id === prefixedAddedId)
770
+ prev.some((id) => testEntityIdsEqual(id, normalizedAddedId))
514
771
  ? prev
515
- : [...prev, prefixedAddedId]
772
+ : [...prev, normalizedAddedId]
516
773
  ));
517
774
  }
518
775
  handleCloseCustomerModal();
@@ -611,17 +868,42 @@ const CommonTestAndPreview = (props) => {
611
868
  messageBody: contentStr,
612
869
  };
613
870
 
614
- case CHANNELS.WEBPUSH:
615
- return {
616
- ...basePayload,
617
- messageBody: contentStr,
618
- };
619
-
620
871
  default:
621
872
  return basePayload;
622
873
  }
623
874
  };
624
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
+
625
907
  /**
626
908
  * Prepare payload for tag extraction based on channel
627
909
  */
@@ -669,12 +951,6 @@ const CommonTestAndPreview = (props) => {
669
951
  templateContent: contentStr,
670
952
  };
671
953
 
672
- case CHANNELS.WEBPUSH:
673
- return {
674
- templateSubject: formDataObj?.content?.title || '',
675
- templateContent: contentStr,
676
- };
677
-
678
954
  case CHANNELS.ZALO: {
679
955
  // For Zalo, extract content from templateListParams array
680
956
  // Combine all variable values into a single string for tag extraction
@@ -722,7 +998,7 @@ const CommonTestAndPreview = (props) => {
722
998
  } = carousel || {};
723
999
  const buttonData = buttons.map((button, index) => {
724
1000
  const {
725
- type, text, phone_number, urlType, url,
1001
+ type, text, phone_number: phoneNumber, urlType, url,
726
1002
  } = button || {};
727
1003
  const buttonObj = {
728
1004
  type,
@@ -730,7 +1006,7 @@ const CommonTestAndPreview = (props) => {
730
1006
  index,
731
1007
  };
732
1008
  if (type === PHONE_NUMBER) {
733
- buttonObj.phoneNumber = phone_number;
1009
+ buttonObj.phoneNumber = phoneNumber;
734
1010
  }
735
1011
  if (type === URL) {
736
1012
  buttonObj.url = url;
@@ -759,7 +1035,133 @@ const CommonTestAndPreview = (props) => {
759
1035
  };
760
1036
  });
761
1037
 
762
- 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
+ ) => {
763
1165
  // Base payload structure common to all channels
764
1166
  const basePayload = {
765
1167
  ouId: -1,
@@ -840,12 +1242,12 @@ const CommonTestAndPreview = (props) => {
840
1242
  },
841
1243
  smsDeliverySettings: {
842
1244
  channelSettings: {
1245
+ channel: CHANNELS.SMS,
843
1246
  gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
844
1247
  domainId: deliverySettingsOverride?.domainId ?? null,
845
1248
  domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
846
1249
  targetNdnc: false,
847
1250
  cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
848
- channel: CHANNELS.SMS,
849
1251
  },
850
1252
  additionalSettings: {
851
1253
  useTinyUrl: false,
@@ -989,7 +1391,7 @@ const CommonTestAndPreview = (props) => {
989
1391
  return {
990
1392
  ...basePayload,
991
1393
  whatsappMessageContent: {
992
- messageBody: templateEditorValue || '',
1394
+ messageBody: resolvedMessageBody || templateEditorValue || '',
993
1395
  accountId: formDataObj?.accountId || '',
994
1396
  sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
995
1397
  accountName: formDataObj?.accountName || '',
@@ -1014,16 +1416,7 @@ const CommonTestAndPreview = (props) => {
1014
1416
  }
1015
1417
 
1016
1418
  case CHANNELS.RCS:
1017
- return {
1018
- ...basePayload,
1019
- rcsMessageContent: {
1020
- channel: CHANNELS.RCS,
1021
- messageBody: contentStr,
1022
- rcsType: additionalProps?.rcsType,
1023
- rcsImageSrc: formDataObj?.rcsImageSrc,
1024
- rcsSuggestions: formDataObj?.rcsSuggestions,
1025
- },
1026
- };
1419
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
1027
1420
 
1028
1421
  case CHANNELS.INAPP: {
1029
1422
  // InApp payload structure similar to MobilePush
@@ -1814,42 +2207,6 @@ const CommonTestAndPreview = (props) => {
1814
2207
  };
1815
2208
  }
1816
2209
 
1817
- case CHANNELS.WEBPUSH: {
1818
- const webpushData = (typeof formDataObj === 'object' && formDataObj !== null)
1819
- ? formDataObj
1820
- : {};
1821
- const innerContent = webpushData?.content || {};
1822
-
1823
- const resolvedTitle = resolveTagsInText(innerContent?.title || '', customValuesObj);
1824
- const resolvedMessage = resolveTagsInText(innerContent?.message || '', customValuesObj);
1825
-
1826
- return {
1827
- ...basePayload,
1828
- webPushMessageContent: {
1829
- channel: CHANNELS.WEBPUSH,
1830
- accountId: webpushData?.accountId || null,
1831
- content: {
1832
- title: resolvedTitle,
1833
- message: resolvedMessage,
1834
- ...(innerContent?.iconImageUrl && { iconImageUrl: innerContent.iconImageUrl }),
1835
- ...(innerContent?.cta && { cta: innerContent.cta }),
1836
- ...(innerContent?.expandableDetails && { expandableDetails: innerContent.expandableDetails }),
1837
- },
1838
- messageSubject: webpushData?.messageSubject || resolvedTitle || '',
1839
- },
1840
- webPushDeliverySettings: {
1841
- channelSettings: {
1842
- channel: CHANNELS.WEBPUSH,
1843
- notificationTtl: {
1844
- duration: 7,
1845
- timeUnit: DAYS,
1846
- },
1847
- },
1848
- additionalSettings: {},
1849
- },
1850
- };
1851
- }
1852
-
1853
2210
  default:
1854
2211
  return basePayload;
1855
2212
  }
@@ -1881,7 +2238,7 @@ const CommonTestAndPreview = (props) => {
1881
2238
  contentObj = hasPreviewCallBeenMade && previewDataHtml?.resolvedBody
1882
2239
  ? previewDataHtml.resolvedBody
1883
2240
  : getCurrentContent || '';
1884
- } else if (channel === CHANNELS.WHATSAPP || channel === CHANNELS.WEBPUSH) {
2241
+ } else if (channel === CHANNELS.WHATSAPP) {
1885
2242
  // For WhatsApp, content is an object with templateMsg, media, CTA, etc.
1886
2243
  // Content comes from WhatsApp component state, passed via content prop
1887
2244
  let resolvedContent = null;
@@ -2161,6 +2518,10 @@ const CommonTestAndPreview = (props) => {
2161
2518
  formatMessage,
2162
2519
  lastModified: formData?.lastModified,
2163
2520
  updatedByName: formData?.updatedByName,
2521
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2522
+ smsFallbackResolvedText,
2523
+ activePreviewTab,
2524
+ onPreviewTabChange: setActivePreviewTab,
2164
2525
  };
2165
2526
  };
2166
2527
 
@@ -2200,7 +2561,12 @@ const CommonTestAndPreview = (props) => {
2200
2561
  }, [show, beeInstance, currentTab, channel]);
2201
2562
 
2202
2563
  /**
2203
- * 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.
2204
2570
  */
2205
2571
  useEffect(() => {
2206
2572
  if (show) {
@@ -2285,7 +2651,61 @@ const CommonTestAndPreview = (props) => {
2285
2651
  actions.getTestGroupsRequested();
2286
2652
  }
2287
2653
  }
2288
- }, [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]);
2289
2709
 
2290
2710
  /**
2291
2711
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -2337,15 +2757,22 @@ const CommonTestAndPreview = (props) => {
2337
2757
  setSelectedCustomer(null);
2338
2758
  setRequiredTags([]);
2339
2759
  setOptionalTags([]);
2760
+ setSmsFallbackExtractedTags([]);
2761
+ setSmsFallbackRequiredTags([]);
2762
+ setSmsFallbackOptionalTags([]);
2763
+ setIsExtractingSmsFallbackTags(false);
2340
2764
  setCustomValues({});
2341
2765
  setShowJSON(false);
2342
2766
  setTagsExtracted(false);
2343
2767
  setPreviewDevice(DESKTOP);
2768
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2769
+ setSmsFallbackPreviewText(undefined);
2344
2770
  setSelectedTestEntities([]);
2345
2771
  actions.clearPrefilledValues();
2346
2772
  } else {
2347
2773
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
2348
2774
  setPreviewDevice(initialDevice);
2775
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2349
2776
  }
2350
2777
  }, [show, initialDevice]);
2351
2778
 
@@ -2354,79 +2781,10 @@ const CommonTestAndPreview = (props) => {
2354
2781
  */
2355
2782
  useEffect(() => {
2356
2783
  if (previewData) {
2357
- setPreviewDataHtml(previewData);
2784
+ setPreviewDataHtml(toPlainPreviewData(previewData));
2358
2785
  }
2359
2786
  }, [previewData]);
2360
2787
 
2361
- /**
2362
- * Process extracted tags and categorize them
2363
- */
2364
- useEffect(() => {
2365
- // Categorize tags into required and optional
2366
- const required = [];
2367
- const optional = [];
2368
- let hasPersonalizationTags = false;
2369
-
2370
- if (extractedTags?.length > 0) {
2371
- const processTag = (tag, parentPath = '') => {
2372
- const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2373
-
2374
- // Skip unsubscribe tag for input fields
2375
- if (tag?.name === UNSUBSCRIBE_TAG_NAME) {
2376
- return;
2377
- }
2378
-
2379
- hasPersonalizationTags = true;
2380
-
2381
- if (tag?.metaData?.userDriven === false) {
2382
- required.push({
2383
- ...tag,
2384
- fullPath: currentPath,
2385
- });
2386
- } else if (tag?.metaData?.userDriven === true) {
2387
- optional.push({
2388
- ...tag,
2389
- fullPath: currentPath,
2390
- });
2391
- }
2392
-
2393
- if (tag?.children?.length > 0) {
2394
- tag.children.forEach((child) => processTag(child, currentPath));
2395
- }
2396
- };
2397
-
2398
- extractedTags.forEach((tag) => processTag(tag));
2399
-
2400
- if (hasPersonalizationTags) {
2401
- setRequiredTags(required);
2402
- setOptionalTags(optional);
2403
- setTagsExtracted(true); // Mark tags as extracted and processed
2404
-
2405
- // Initialize custom values for required tags
2406
- const initialValues = {};
2407
- required.forEach((tag) => {
2408
- initialValues[tag?.fullPath] = '';
2409
- });
2410
- optional.forEach((tag) => {
2411
- initialValues[tag?.fullPath] = '';
2412
- });
2413
- setCustomValues(initialValues);
2414
- } else {
2415
- // Reset all tag-related state if no personalization tags
2416
- setRequiredTags([]);
2417
- setOptionalTags([]);
2418
- setCustomValues({});
2419
- setTagsExtracted(false);
2420
- }
2421
- } else {
2422
- // Reset all tag-related state if no tags
2423
- setRequiredTags([]);
2424
- setOptionalTags([]);
2425
- setCustomValues({});
2426
- setTagsExtracted(false);
2427
- }
2428
- }, [extractedTags]);
2429
-
2430
2788
  /**
2431
2789
  * Handle customer selection and fetch prefilled values
2432
2790
  */
@@ -2434,17 +2792,15 @@ const CommonTestAndPreview = (props) => {
2434
2792
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2435
2793
  setTagsExtracted(true); // Auto-open custom values editor
2436
2794
 
2437
- // Get all available tags
2438
- const allTags = [...requiredTags, ...optionalTags];
2439
2795
  const requiredTagObj = {};
2440
- requiredTags.forEach((tag) => {
2796
+ allRequiredTags.forEach((tag) => {
2441
2797
  requiredTagObj[tag?.fullPath] = '';
2442
2798
  });
2443
2799
  if (allTags.length > 0) {
2444
2800
  const payload = preparePreviewPayload(
2445
- channel,
2801
+ activeChannelForActions,
2446
2802
  formData || {},
2447
- getCurrentContent,
2803
+ activeContentForActions,
2448
2804
  {
2449
2805
  ...requiredTagObj,
2450
2806
  },
@@ -2453,7 +2809,7 @@ const CommonTestAndPreview = (props) => {
2453
2809
  actions.getPrefilledValuesRequested(payload);
2454
2810
  }
2455
2811
  }
2456
- }, [selectedCustomer]);
2812
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2457
2813
 
2458
2814
  /**
2459
2815
  * Update custom values with prefilled values from API
@@ -2464,24 +2820,29 @@ const CommonTestAndPreview = (props) => {
2464
2820
  if (prefilledValues && selectedCustomer) {
2465
2821
  // Always replace all values with prefilled values
2466
2822
  const updatedValues = {};
2467
- [...requiredTags, ...optionalTags].forEach((tag) => {
2823
+ allTags.forEach((tag) => {
2468
2824
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2469
2825
  });
2470
2826
 
2471
2827
  setCustomValues(updatedValues);
2472
2828
 
2473
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;
2474
2834
  const payload = preparePreviewPayload(
2475
- channel,
2835
+ previewChannelForPrefill,
2476
2836
  formData || {},
2477
- getCurrentContent,
2837
+ previewContentForPrefill,
2478
2838
  updatedValues,
2479
2839
  selectedCustomer
2480
2840
  );
2481
2841
  actions.updatePreviewRequested(payload);
2482
2842
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2843
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2483
2844
  }
2484
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2845
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2485
2846
 
2486
2847
  /**
2487
2848
  * Map channel constants to display names (lowercase for message)
@@ -2575,11 +2936,7 @@ const CommonTestAndPreview = (props) => {
2575
2936
  setTagsExtracted(true); // Auto-open custom values editor
2576
2937
 
2577
2938
  // Clear any existing values while waiting for prefilled values
2578
- const emptyValues = {};
2579
- [...requiredTags, ...optionalTags].forEach((tag) => {
2580
- emptyValues[tag?.fullPath] = '';
2581
- });
2582
- setCustomValues(emptyValues);
2939
+ setCustomValues(buildEmptyValues());
2583
2940
  };
2584
2941
 
2585
2942
  /**
@@ -2593,11 +2950,7 @@ const CommonTestAndPreview = (props) => {
2593
2950
  actions.clearPreviewErrors();
2594
2951
 
2595
2952
  // Initialize empty values for all tags
2596
- const emptyValues = {};
2597
- [...requiredTags, ...optionalTags].forEach((tag) => {
2598
- emptyValues[tag?.fullPath] = '';
2599
- });
2600
- setCustomValues(emptyValues);
2953
+ setCustomValues(buildEmptyValues());
2601
2954
 
2602
2955
  // Don't make preview call when clearing selection - just reset to raw content
2603
2956
  // Preview will be shown using raw formData/content
@@ -2633,17 +2986,20 @@ const CommonTestAndPreview = (props) => {
2633
2986
  */
2634
2987
  const handleDiscardCustomValues = () => {
2635
2988
  // Initialize empty values for all tags
2636
- const emptyValues = {};
2637
- [...requiredTags, ...optionalTags].forEach((tag) => {
2638
- emptyValues[tag?.fullPath] = '';
2639
- });
2989
+ const emptyValues = buildEmptyValues();
2640
2990
  setCustomValues(emptyValues);
2641
2991
 
2992
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
2993
+ setSmsFallbackPreviewText(undefined);
2994
+
2642
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;
2643
2999
  const payload = preparePreviewPayload(
2644
- channel,
3000
+ previewChannelForDiscard,
2645
3001
  formData || {},
2646
- getCurrentContent,
3002
+ previewContentForDiscard,
2647
3003
  emptyValues,
2648
3004
  selectedCustomer
2649
3005
  );
@@ -2657,14 +3013,21 @@ const CommonTestAndPreview = (props) => {
2657
3013
  */
2658
3014
  const handleUpdatePreview = async () => {
2659
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;
2660
3021
  const payload = preparePreviewPayload(
2661
- channel,
3022
+ previewChannel,
2662
3023
  formData || {},
2663
- getCurrentContent,
3024
+ previewContent,
2664
3025
  customValues,
2665
3026
  selectedCustomer
2666
3027
  );
2667
3028
  await actions.updatePreviewRequested(payload);
3029
+
3030
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2668
3031
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2669
3032
  } catch (error) {
2670
3033
  CapNotification.error({
@@ -2674,25 +3037,115 @@ const CommonTestAndPreview = (props) => {
2674
3037
  };
2675
3038
 
2676
3039
  /**
2677
- * Handle extract tags
3040
+ * Categorize extracted tags into required/optional.
2678
3041
  */
2679
- const handleExtractTags = () => {
2680
- // Get content based on channel
2681
- 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
+ };
2682
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;
2683
3125
  if (channel === CHANNELS.EMAIL && formData) {
2684
3126
  const currentTabData = formData[currentTab - 1];
2685
3127
  const activeTab = currentTabData?.activeTab;
2686
3128
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2687
3129
  contentToExtract = templateContent || contentToExtract;
2688
3130
  }
3131
+ return contentToExtract;
3132
+ };
3133
+
3134
+ /**
3135
+ * Handle extract tags
3136
+ */
3137
+ const handleExtractTags = () => {
3138
+ if (channel === CHANNELS.RCS) {
3139
+ applyRcsSmsFallbackTagExtraction();
3140
+ return;
3141
+ }
2689
3142
 
2690
- // Check for personalization tags (excluding unsubscribe)
3143
+ const contentToExtract = getContentForTagExtraction();
2691
3144
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2692
3145
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3146
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2693
3147
 
2694
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2695
- // If only unsubscribe tag is present, show noTagsExtracted message
3148
+ if (onlyUnsubscribe) {
2696
3149
  setTagsExtracted(false);
2697
3150
  setRequiredTags([]);
2698
3151
  setOptionalTags([]);
@@ -2700,10 +3153,9 @@ const CommonTestAndPreview = (props) => {
2700
3153
  return;
2701
3154
  }
2702
3155
 
2703
- // Extract tags
2704
3156
  setTagsExtracted(true);
2705
3157
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2706
- channel,
3158
+ activeChannelForActions,
2707
3159
  formData || {},
2708
3160
  contentToExtract
2709
3161
  );
@@ -2765,7 +3217,7 @@ const CommonTestAndPreview = (props) => {
2765
3217
  if (existingTestCustomer) {
2766
3218
  const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2767
3219
  if (entityId != null) {
2768
- const id = 'customer:' + normalizeTestEntityId(entityId);
3220
+ const id = normalizeTestEntityId(entityId);
2769
3221
  setSelectedTestEntities((prev) => (
2770
3222
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2771
3223
  ));
@@ -2802,7 +3254,7 @@ const CommonTestAndPreview = (props) => {
2802
3254
  (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
2803
3255
  );
2804
3256
  if (alreadyInTestListByCustomerId) {
2805
- const id = 'customer:' + normalizeTestEntityId(customerIdFromLookup);
3257
+ const id = normalizeTestEntityId(customerIdFromLookup);
2806
3258
  setSelectedTestEntities((prev) => (
2807
3259
  prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
2808
3260
  ));
@@ -2839,26 +3291,21 @@ const CommonTestAndPreview = (props) => {
2839
3291
  const handleSendTestMessage = () => {
2840
3292
  const allUserIds = [];
2841
3293
  selectedTestEntities.forEach((entityId) => {
2842
- if (String(entityId).startsWith('group:')) {
2843
- const rawId = String(entityId).slice('group:'.length);
2844
- const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, rawId));
2845
- if (group) {
2846
- allUserIds.push(...group.userIds);
2847
- }
3294
+ const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
3295
+ if (group) {
3296
+ allUserIds.push(...group.userIds);
2848
3297
  } else {
2849
- const rawId = String(entityId).startsWith('customer:')
2850
- ? String(entityId).slice('customer:'.length)
2851
- : String(entityId);
2852
- allUserIds.push(Number(rawId));
3298
+ allUserIds.push(entityId);
2853
3299
  }
2854
3300
  });
2855
3301
  const uniqueUserIds = [...new Set(allUserIds)];
2856
3302
 
2857
- const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
3303
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
2858
3304
  ? testPreviewDeliverySettings[channel]
2859
3305
  : null;
2860
3306
 
2861
- // 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.
2862
3309
  const initialPayload = prepareTestMessagePayload(
2863
3310
  channel,
2864
3311
  formData || content || {},
@@ -2866,7 +3313,8 @@ const CommonTestAndPreview = (props) => {
2866
3313
  customValues,
2867
3314
  uniqueUserIds,
2868
3315
  previewData,
2869
- deliveryOverride
3316
+ deliveryOverride,
3317
+ {},
2870
3318
  );
2871
3319
 
2872
3320
  actions.createMessageMetaRequested(
@@ -2899,11 +3347,10 @@ const CommonTestAndPreview = (props) => {
2899
3347
  // ============================================
2900
3348
  // RENDER HELPER FUNCTIONS
2901
3349
  // ============================================
2902
-
2903
3350
  const renderLeftPanelContent = () => (
2904
3351
  <LeftPanelContent
2905
- isExtractingTags={isExtractingTags}
2906
- extractedTags={extractedTags}
3352
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3353
+ extractedTags={leftPanelExtractedTags}
2907
3354
  selectedCustomer={selectedCustomer}
2908
3355
  handleCustomerSelect={handleCustomerSelect}
2909
3356
  handleSearchCustomer={handleSearchCustomer}
@@ -2921,15 +3368,29 @@ const CommonTestAndPreview = (props) => {
2921
3368
 
2922
3369
  const renderCustomValuesEditor = () => (
2923
3370
  <CustomValuesEditor
2924
- isExtractingTags={isExtractingTags}
3371
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
2925
3372
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
2926
3373
  showJSON={showJSON}
2927
3374
  setShowJSON={setShowJSON}
2928
3375
  customValues={customValues}
2929
3376
  handleJSONTextChange={handleJSONTextChange}
2930
- extractedTags={extractedTags}
2931
- requiredTags={requiredTags}
2932
- 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
+ ]}
2933
3394
  handleCustomValueChange={handleCustomValueChange}
2934
3395
  handleDiscardCustomValues={handleDiscardCustomValues}
2935
3396
  handleUpdatePreview={handleUpdatePreview}
@@ -2945,18 +3406,13 @@ const CommonTestAndPreview = (props) => {
2945
3406
  }));
2946
3407
  };
2947
3408
 
2948
- /** 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. */
2949
3410
  const handleTestCustomersSearch = useCallback((value) => {
2950
3411
  if (value == null || value === '') {
2951
3412
  setSearchValue('');
2952
3413
  return;
2953
3414
  }
2954
- const raw = String(value).trim();
2955
- if (channel === CHANNELS.SMS) {
2956
- setSearchValue(formatPhoneNumber(raw));
2957
- } else {
2958
- setSearchValue(raw);
2959
- }
3415
+ setSearchValue(String(value).trim());
2960
3416
  }, [channel]);
2961
3417
 
2962
3418
  const renderSendTestMessage = () => (
@@ -2974,12 +3430,13 @@ const CommonTestAndPreview = (props) => {
2974
3430
  renderAddTestCustomerButton={renderAddTestCustomerButton}
2975
3431
  formatMessage={formatMessage}
2976
3432
  deliverySettings={testPreviewDeliverySettings[channel]}
2977
- senderDetailsOptions={senderDetailsByChannel[channel]}
3433
+ senderDetailsByChannel={senderDetailsByChannel}
2978
3434
  wecrmAccounts={wecrmAccounts}
2979
3435
  onSaveDeliverySettings={handleSaveDeliverySettings}
2980
3436
  isLoadingSenderDetails={isLoadingSenderDetails}
2981
3437
  smsTraiDltEnabled={smsTraiDltEnabled}
2982
3438
  registeredSenderIds={registeredSenderIds}
3439
+ isChannelSmsFallbackPreviewEnabled={isRcsSmsFallbackPreviewEnabled}
2983
3440
  searchValue={searchValue}
2984
3441
  setSearchValue={handleTestCustomersSearch}
2985
3442
  />
@@ -2993,14 +3450,13 @@ const CommonTestAndPreview = (props) => {
2993
3450
 
2994
3451
  const renderAddTestCustomerButton = () => {
2995
3452
  const raw = (searchValue || '').trim();
2996
- const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
2997
3453
  const showAddButton =
2998
3454
  [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
2999
- (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
3455
+ (channel === CHANNELS.EMAIL ? isValidEmail(raw) : isValidMobile(formatPhoneNumber(raw)));
3000
3456
  if (!showAddButton) return null;
3001
3457
  return (
3002
3458
  <AddTestCustomerButton
3003
- searchValue={value}
3459
+ searchValue={channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw}
3004
3460
  handleAddTestCustomer={handleAddTestCustomer}
3005
3461
  />
3006
3462
  );