cap-creatives-ui 8.0.280 → 8.0.321

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 (247) hide show
  1. package/.github/workflows/pr-title-check.yml +88 -0
  2. package/app/constants/unified.js +21 -1
  3. package/app/containers/App/constants.js +0 -1
  4. package/app/containers/Login/test/index.test.js +123 -0
  5. package/app/containers/Login/test/selectors.test.js +165 -0
  6. package/app/initialState.js +0 -2
  7. package/app/services/api.js +6 -0
  8. package/app/services/tests/api.test.js +7 -0
  9. package/app/services/tests/getSchema.test.js +95 -0
  10. package/app/utils/common.js +23 -9
  11. package/app/utils/commonUtils.js +64 -93
  12. package/app/utils/tagValidations.js +83 -219
  13. package/app/utils/templateVarUtils.js +172 -0
  14. package/app/utils/tests/common.test.js +265 -323
  15. package/app/utils/tests/commonUtil.test.js +461 -118
  16. package/app/utils/tests/commonUtils.test.js +581 -0
  17. package/app/utils/tests/messageUtils.test.js +95 -0
  18. package/app/utils/tests/smsCharCount.test.js +304 -0
  19. package/app/utils/tests/smsCharCountV2.test.js +213 -10
  20. package/app/utils/tests/tagValidations.test.js +474 -357
  21. package/app/utils/tests/templateVarUtils.test.js +160 -0
  22. package/app/v2Components/CapDeviceContent/index.js +10 -7
  23. package/app/v2Components/CapTagList/index.js +32 -24
  24. package/app/v2Components/CapTagList/style.scss +48 -0
  25. package/app/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
  26. package/app/v2Components/CapTagListWithInput/index.js +8 -0
  27. package/app/v2Components/CapWhatsappCTA/index.js +2 -0
  28. package/app/v2Components/CapWhatsappCarouselButton/index.js +32 -14
  29. package/app/v2Components/CapWhatsappCarouselButton/tests/index.test.js +120 -2
  30. package/app/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  31. package/app/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +39 -0
  32. package/app/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +606 -0
  33. package/app/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.scss +36 -0
  34. package/app/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +79 -0
  35. package/app/v2Components/CommonTestAndPreview/DeliverySettings/index.js +314 -0
  36. package/app/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +141 -0
  37. package/app/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +156 -0
  38. package/app/v2Components/CommonTestAndPreview/SendTestMessage.js +57 -1
  39. package/app/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  40. package/app/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  41. package/app/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +210 -4
  42. package/app/v2Components/CommonTestAndPreview/actions.js +20 -0
  43. package/app/v2Components/CommonTestAndPreview/constants.js +57 -1
  44. package/app/v2Components/CommonTestAndPreview/index.js +878 -156
  45. package/app/v2Components/CommonTestAndPreview/messages.js +41 -3
  46. package/app/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  47. package/app/v2Components/CommonTestAndPreview/reducer.js +47 -0
  48. package/app/v2Components/CommonTestAndPreview/sagas.js +75 -5
  49. package/app/v2Components/CommonTestAndPreview/selectors.js +51 -0
  50. package/app/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  51. package/app/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +1156 -0
  52. package/app/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +334 -0
  53. package/app/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +576 -0
  54. package/app/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +156 -0
  55. package/app/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  56. package/app/v2Components/CommonTestAndPreview/tests/actions.test.js +50 -0
  57. package/app/v2Components/CommonTestAndPreview/tests/constants.test.js +18 -7
  58. package/app/v2Components/CommonTestAndPreview/tests/index.test.js +914 -5
  59. package/app/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  60. package/app/v2Components/CommonTestAndPreview/tests/reducer.test.js +118 -0
  61. package/app/v2Components/CommonTestAndPreview/tests/sagas.test.js +146 -378
  62. package/app/v2Components/CommonTestAndPreview/tests/selectors.test.js +146 -0
  63. package/app/v2Components/ErrorInfoNote/index.js +24 -26
  64. package/app/v2Components/FormBuilder/index.js +182 -204
  65. package/app/v2Components/FormBuilder/messages.js +4 -8
  66. package/app/v2Components/HtmlEditor/HTMLEditor.js +7 -6
  67. package/app/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -1
  68. package/app/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +928 -17
  69. package/app/v2Components/HtmlEditor/components/CodeEditorPane/index.js +4 -2
  70. package/app/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +452 -3
  71. package/app/v2Components/HtmlEditor/hooks/useValidation.js +12 -9
  72. package/app/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +132 -0
  73. package/app/v2Components/HtmlEditor/utils/htmlValidator.js +4 -2
  74. package/app/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  75. package/app/v2Components/SmsFallback/constants.js +73 -0
  76. package/app/v2Components/SmsFallback/index.js +956 -0
  77. package/app/v2Components/SmsFallback/index.scss +265 -0
  78. package/app/v2Components/SmsFallback/messages.js +78 -0
  79. package/app/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  80. package/app/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  81. package/app/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  82. package/app/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  83. package/app/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  84. package/app/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  85. package/app/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  86. package/app/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  87. package/app/v2Components/TestAndPreviewSlidebox/index.js +22 -1
  88. package/app/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  89. package/app/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  90. package/app/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  91. package/app/v2Components/VarSegmentMessageEditor/index.js +125 -0
  92. package/app/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  93. package/app/v2Containers/BeeEditor/index.js +3 -0
  94. package/app/v2Containers/BeePopupEditor/index.js +9 -2
  95. package/app/v2Containers/Cap/mockData.js +0 -14
  96. package/app/v2Containers/Cap/reducer.js +3 -55
  97. package/app/v2Containers/Cap/tests/reducer.test.js +0 -102
  98. package/app/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  99. package/app/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  100. package/app/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  101. package/app/v2Containers/CommunicationFlow/constants.js +200 -0
  102. package/app/v2Containers/CommunicationFlow/index.js +102 -0
  103. package/app/v2Containers/CommunicationFlow/messages.js +346 -0
  104. package/app/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  105. package/app/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  106. package/app/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  107. package/app/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  108. package/app/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  109. package/app/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  110. package/app/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  111. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  112. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  113. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  114. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  115. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  116. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  117. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  118. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  119. package/app/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  120. package/app/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  121. package/app/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  122. package/app/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  123. package/app/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  124. package/app/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  125. package/app/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  126. package/app/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  127. package/app/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  128. package/app/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  129. package/app/v2Containers/CreativesContainer/SlideBoxContent.js +127 -11
  130. package/app/v2Containers/CreativesContainer/SlideBoxFooter.js +62 -9
  131. package/app/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  132. package/app/v2Containers/CreativesContainer/constants.js +24 -0
  133. package/app/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  134. package/app/v2Containers/CreativesContainer/index.js +346 -71
  135. package/app/v2Containers/CreativesContainer/index.scss +51 -1
  136. package/app/v2Containers/CreativesContainer/messages.js +12 -0
  137. package/app/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  138. package/app/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +69 -1
  139. package/app/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +443 -0
  140. package/app/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  141. package/app/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +147 -4
  142. package/app/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  143. package/app/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +57 -10
  144. package/app/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  145. package/app/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  146. package/app/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  147. package/app/v2Containers/Email/index.js +2 -5
  148. package/app/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +58 -77
  149. package/app/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  150. package/app/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +158 -89
  151. package/app/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
  152. package/app/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +17 -12
  153. package/app/v2Containers/EmailWrapper/index.js +4 -0
  154. package/app/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  155. package/app/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +133 -0
  156. package/app/v2Containers/FTP/index.js +2 -51
  157. package/app/v2Containers/FTP/messages.js +0 -4
  158. package/app/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +110 -155
  159. package/app/v2Containers/InApp/index.js +297 -118
  160. package/app/v2Containers/InApp/tests/index.test.js +17 -6
  161. package/app/v2Containers/InApp/tests/mockData.js +1 -1
  162. package/app/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +19 -0
  163. package/app/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +3 -0
  164. package/app/v2Containers/InAppWrapper/index.js +3 -0
  165. package/app/v2Containers/InappAdvance/index.js +5 -104
  166. package/app/v2Containers/InappAdvance/tests/index.test.js +2 -0
  167. package/app/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +24 -3
  168. package/app/v2Containers/Line/Container/Text/index.js +0 -1
  169. package/app/v2Containers/MobilePush/Create/index.js +105 -28
  170. package/app/v2Containers/MobilePush/Create/messages.js +4 -0
  171. package/app/v2Containers/MobilePush/Edit/index.js +250 -68
  172. package/app/v2Containers/MobilePush/Edit/messages.js +4 -0
  173. package/app/v2Containers/MobilePushNew/components/PlatformContentFields.js +36 -12
  174. package/app/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +68 -27
  175. package/app/v2Containers/MobilePushNew/index.js +78 -35
  176. package/app/v2Containers/MobilePushNew/messages.js +8 -0
  177. package/app/v2Containers/MobilepushWrapper/index.js +11 -1
  178. package/app/v2Containers/Rcs/constants.js +32 -1
  179. package/app/v2Containers/Rcs/index.js +963 -916
  180. package/app/v2Containers/Rcs/index.scss +85 -6
  181. package/app/v2Containers/Rcs/messages.js +10 -1
  182. package/app/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  183. package/app/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +41136 -1566
  184. package/app/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  185. package/app/v2Containers/Rcs/tests/index.test.js +41 -38
  186. package/app/v2Containers/Rcs/tests/mockData.js +38 -0
  187. package/app/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  188. package/app/v2Containers/Rcs/tests/utils.test.js +379 -1
  189. package/app/v2Containers/Rcs/utils.js +358 -10
  190. package/app/v2Containers/Sms/Create/index.js +122 -39
  191. package/app/v2Containers/Sms/Create/messages.js +4 -0
  192. package/app/v2Containers/Sms/Edit/index.js +37 -3
  193. package/app/v2Containers/Sms/commonMethods.js +3 -6
  194. package/app/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  195. package/app/v2Containers/Sms/tests/commonMethods.test.js +122 -0
  196. package/app/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  197. package/app/v2Containers/SmsTrai/Create/index.js +9 -4
  198. package/app/v2Containers/SmsTrai/Create/index.scss +1 -1
  199. package/app/v2Containers/SmsTrai/Edit/constants.js +2 -0
  200. package/app/v2Containers/SmsTrai/Edit/index.js +667 -160
  201. package/app/v2Containers/SmsTrai/Edit/index.scss +121 -0
  202. package/app/v2Containers/SmsTrai/Edit/messages.js +9 -4
  203. package/app/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4590 -2436
  204. package/app/v2Containers/SmsWrapper/index.js +41 -8
  205. package/app/v2Containers/TagList/index.js +63 -2
  206. package/app/v2Containers/TagList/messages.js +8 -0
  207. package/app/v2Containers/TagList/tests/TagList.test.js +122 -20
  208. package/app/v2Containers/TagList/tests/mockdata.js +17 -0
  209. package/app/v2Containers/Templates/TemplatesActionBar.js +101 -0
  210. package/app/v2Containers/Templates/_templates.scss +61 -2
  211. package/app/v2Containers/Templates/actions.js +11 -0
  212. package/app/v2Containers/Templates/constants.js +2 -0
  213. package/app/v2Containers/Templates/index.js +90 -40
  214. package/app/v2Containers/Templates/reducer.js +3 -1
  215. package/app/v2Containers/Templates/sagas.js +57 -12
  216. package/app/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  217. package/app/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  218. package/app/v2Containers/Templates/tests/reducer.test.js +12 -0
  219. package/app/v2Containers/Templates/tests/sagas.test.js +193 -12
  220. package/app/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  221. package/app/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  222. package/app/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  223. package/app/v2Containers/TemplatesV2/index.js +147 -49
  224. package/app/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  225. package/app/v2Containers/Viber/index.js +9 -10
  226. package/app/v2Containers/Viber/index.scss +1 -1
  227. package/app/v2Containers/WebPush/Create/components/BrandIconSection.test.js +264 -0
  228. package/app/v2Containers/WebPush/Create/components/MessageSection.js +78 -19
  229. package/app/v2Containers/WebPush/Create/components/MessageSection.test.js +82 -0
  230. package/app/v2Containers/WebPush/Create/components/__snapshots__/BrandIconSection.test.js.snap +187 -0
  231. package/app/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +25 -17
  232. package/app/v2Containers/WebPush/Create/hooks/useAiraTriggerPosition.js +80 -0
  233. package/app/v2Containers/WebPush/Create/hooks/useAiraTriggerPosition.test.js +210 -0
  234. package/app/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -5
  235. package/app/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  236. package/app/v2Containers/WebPush/Create/index.js +36 -6
  237. package/app/v2Containers/WebPush/Create/index.scss +5 -0
  238. package/app/v2Containers/WebPush/Create/messages.js +8 -1
  239. package/app/v2Containers/WebPush/Create/preview/tests/NotificationContainer.test.js +269 -0
  240. package/app/v2Containers/WebPush/Create/utils/validation.js +31 -15
  241. package/app/v2Containers/WebPush/Create/utils/validation.test.js +72 -24
  242. package/app/v2Containers/Whatsapp/index.js +28 -53
  243. package/app/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +26939 -3982
  244. package/app/v2Containers/Whatsapp/tests/index.test.js +172 -0
  245. package/app/v2Containers/Zalo/index.js +5 -11
  246. package/package.json +2 -2
  247. package/version +9 -0
@@ -8,7 +8,7 @@
8
8
 
9
9
  import PropTypes from 'prop-types';
10
10
  import React, {
11
- useState, useEffect, useMemo, useRef,
11
+ useState, useEffect, useMemo, useRef, useCallback,
12
12
  } from 'react';
13
13
  import { FormattedMessage } from 'react-intl';
14
14
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
@@ -28,7 +28,8 @@ import CustomValuesEditor from './CustomValuesEditor';
28
28
  import SendTestMessage from './SendTestMessage';
29
29
  import PreviewSection from './PreviewSection';
30
30
 
31
- // Import constants
31
+ import * as Api from '../../services/api';
32
+ import { extractTemplateVariables } from '../../utils/templateVarUtils';
32
33
  import {
33
34
  CHANNELS,
34
35
  TEST,
@@ -65,10 +66,125 @@ import {
65
66
  IN_APP_CHANNEL_NAME,
66
67
  MOBILE_PUSH_CHANNEL_NAME,
67
68
  CHANNEL,
69
+ PHONE_NUMBER,
70
+ DYNAMIC_URL,
71
+ IMAGE,
72
+ VIDEO,
73
+ URL,
74
+ PREVIEW_TAB_RCS,
75
+ PREVIEW_TAB_SMS_FALLBACK,
76
+ CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
77
+ RCS_TEST_META_CONTENT_TYPE_RICHCARD,
78
+ RCS_TEST_META_CARD_TYPE_STANDALONE,
79
+ RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
80
+ RCS_TEST_META_CARD_WIDTH_SMALL,
81
+ SMS_MUSTACHE_TAG_PATTERN,
68
82
  } from './constants';
69
-
70
- // Import utilities
71
83
  import { getCdnUrl } from '../../utils/cdnTransformation';
84
+ import {
85
+ normalizePreviewApiPayload,
86
+ extractPreviewFromLiquidResponse,
87
+ getSmsFallbackTextForTagExtraction,
88
+ } from './previewApiUtils';
89
+
90
+ /**
91
+ * Drop empty GSM rows. RCS/DLT responses often set gsm_sender_id equal to domainName — keep those rows
92
+ * for RCS defaults (ModifyDeliverySettings uses the same rule for the sender dropdown).
93
+ */
94
+ const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEchoFilter = false } = {}) => {
95
+ const normalizedDomainName =
96
+ domain?.domainName != null ? String(domain.domainName).trim().toLowerCase() : '';
97
+ return (gsmSenders || []).filter((gsmSenderRow) => {
98
+ const rawValue = gsmSenderRow?.value;
99
+ if (rawValue == null) return false;
100
+ const trimmedSenderValue = String(rawValue).trim();
101
+ if (!trimmedSenderValue) return false;
102
+ const senderMatchesDomainLabel =
103
+ normalizedDomainName && trimmedSenderValue.toLowerCase() === normalizedDomainName;
104
+ if (!skipDomainNameEchoFilter && senderMatchesDomainLabel) return false;
105
+ return true;
106
+ });
107
+ };
108
+
109
+ /** Preview payload from Redux may be an Immutable Map — normalize for React state. */
110
+ const toPlainPreviewData = (data) => {
111
+ if (data == null) return null;
112
+ const plain = typeof data.toJS === 'function' ? data.toJS() : data;
113
+ return normalizePreviewApiPayload(plain);
114
+ };
115
+
116
+ /**
117
+ * Merge existing customValues with tag keys from categorized groups.
118
+ * Each group is { required, optional } (arrays of tag objects with fullPath).
119
+ * Preserves existing values; adds '' for any tag key not yet present.
120
+ * Reusable for RCS+fallback and any flow that needs to ensure customValues has keys for tags.
121
+ */
122
+ const mergeCustomValuesWithTagKeys = (prev, ...categorizedGroups) => {
123
+ const next = { ...(prev || {}) };
124
+ categorizedGroups.forEach((group) => {
125
+ [...(group.required || []), ...(group.optional || [])].forEach((tag) => {
126
+ const key = tag?.fullPath;
127
+ if (key && next[key] === undefined) next[key] = '';
128
+ });
129
+ });
130
+ return next;
131
+ };
132
+
133
+ /** True when `body` contains `{{name}}` mustache tokens (user-fillable personalization tags).
134
+ * DLT `{#name#}` slots are pre-bound template variables and are intentionally excluded. */
135
+ const smsTemplateHasMustacheTags = (body) =>
136
+ typeof body === 'string' && SMS_MUSTACHE_TAG_PATTERN.test(body);
137
+
138
+ /**
139
+ * Build tag rows from `{{…}}` mustache tokens only — DLT `{#…#}` slots are excluded because
140
+ * they are pre-bound template variables, not user-fillable personalization tags.
141
+ * Passing a mustache-only captureRegex to extractTemplateVariables skips the DLT branch.
142
+ * A non-global regex is used so ensureGlobalRegexForExecLoop creates a fresh instance on each call.
143
+ */
144
+ const buildSyntheticSmsMustacheTags = (body = '') => {
145
+ if (!body || typeof body !== 'string') return [];
146
+ return extractTemplateVariables(body, /\{\{([^}]+)\}\}/).map((name) => ({
147
+ name,
148
+ metaData: { userDriven: false },
149
+ children: [],
150
+ }));
151
+ };
152
+
153
+ /** RCS createMessageMeta: media shape (mediaUrl, thumbnailUrl, height string). */
154
+ const normalizeRcsTestCardMedia = (media) => {
155
+ if (!media || typeof media !== 'object') return undefined;
156
+ const mediaUrl =
157
+ media.mediaUrl != null && String(media.mediaUrl).trim() !== ''
158
+ ? String(media.mediaUrl)
159
+ : media.url != null && String(media.url).trim() !== ''
160
+ ? String(media.url)
161
+ : '';
162
+ const thumbnailUrl = media.thumbnailUrl != null ? String(media.thumbnailUrl) : '';
163
+ const height = media.height != null ? String(media.height) : undefined;
164
+ const out = { mediaUrl, thumbnailUrl };
165
+ if (height) out.height = height;
166
+ return out;
167
+ };
168
+
169
+ /** RCS createMessageMeta: suggestion shape (index, type, text, phoneNumber, url, postback). */
170
+ const mapRcsSuggestionForTestMeta = (suggestionRow, index) => ({
171
+ index,
172
+ type: suggestionRow?.type ?? '',
173
+ text: suggestionRow?.text != null ? String(suggestionRow.text) : '',
174
+ phoneNumber:
175
+ suggestionRow?.phoneNumber != null
176
+ ? String(suggestionRow.phoneNumber)
177
+ : suggestionRow?.phone_number != null
178
+ ? String(suggestionRow.phone_number)
179
+ : '',
180
+ url: suggestionRow?.url !== undefined ? suggestionRow.url : null,
181
+ postback:
182
+ suggestionRow?.postback != null
183
+ ? String(suggestionRow.postback)
184
+ : suggestionRow?.text != null
185
+ ? String(suggestionRow.text)
186
+ : '',
187
+ });
72
188
 
73
189
  /**
74
190
  * Preview Component Factory - REMOVED IN PHASE 5
@@ -80,7 +196,7 @@ import { getCdnUrl } from '../../utils/cdnTransformation';
80
196
  */
81
197
  const CommonTestAndPreview = (props) => {
82
198
  const {
83
- intl: { formatMessage },
199
+ intl: { formatMessage, locale: userLocale = 'en' },
84
200
  show,
85
201
  onClose,
86
202
  channel, // The channel: 'EMAIL', 'SMS', 'RCS', etc.
@@ -105,6 +221,10 @@ const CommonTestAndPreview = (props) => {
105
221
  updatePreviewErrors,
106
222
  fetchPrefilledValuesError,
107
223
  fetchPrefilledValuesErrors,
224
+ senderDetailsByChannel = {},
225
+ wecrmAccounts = [],
226
+ isLoadingSenderDetails = false,
227
+ orgUnitId = -1,
108
228
  // Email-specific props
109
229
  beeInstance,
110
230
  currentTab = 1,
@@ -112,19 +232,29 @@ const CommonTestAndPreview = (props) => {
112
232
  ...additionalProps
113
233
  } = props;
114
234
 
235
+ const smsFallbackContent = additionalProps?.smsFallbackContent;
236
+ const smsFallbackTextForTagExtraction = useMemo(
237
+ () => getSmsFallbackTextForTagExtraction(smsFallbackContent),
238
+ [smsFallbackContent],
239
+ );
115
240
  // ============================================
116
241
  // STATE MANAGEMENT
117
242
  // ============================================
118
243
  const [selectedCustomer, setSelectedCustomer] = useState(null);
119
244
  const [requiredTags, setRequiredTags] = useState([]);
120
245
  const [optionalTags, setOptionalTags] = useState([]);
246
+ const [smsFallbackExtractedTags, setSmsFallbackExtractedTags] = useState([]);
247
+ const [smsFallbackRequiredTags, setSmsFallbackRequiredTags] = useState([]);
248
+ const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
249
+ const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
121
250
  const [customValues, setCustomValues] = useState({});
122
251
  const [showJSON, setShowJSON] = useState(false);
123
252
  const [tagsExtracted, setTagsExtracted] = useState(false);
124
- // Initialize device based on channel: SMS uses Android/iOS, others use Desktop/Mobile
125
- // Initialize device based on channel: SMS, WhatsApp, RCS, InApp, MobilePush, and Viber use Android/iOS, others use Desktop/Mobile
126
- const initialDevice = (channel === CHANNELS.SMS || channel === CHANNELS.WHATSAPP || channel === CHANNELS.RCS || channel === CHANNELS.INAPP || channel === CHANNELS.MOBILEPUSH || channel === CHANNELS.VIBER) ? ANDROID : DESKTOP;
253
+
254
+ const initialDevice = CHANNELS_USING_ANDROID_PREVIEW_DEVICE.includes(channel) ? ANDROID : DESKTOP;
127
255
  const [previewDevice, setPreviewDevice] = useState(initialDevice);
256
+ const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
257
+ const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
128
258
  // Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
129
259
  const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
130
260
  const [previewDataHtml, setPreviewDataHtml] = useState(() => {
@@ -146,15 +276,205 @@ const CommonTestAndPreview = (props) => {
146
276
  const [selectedTestEntities, setSelectedTestEntities] = useState([]);
147
277
  const [beeContent, setBeeContent] = useState(''); // Track BEE editor content separately (EMAIL only)
148
278
  const previousBeeContentRef = useRef(''); // Track previous BEE content (EMAIL only)
279
+ // Delivery settings for Test and Preview (SMS, Email, WhatsApp) — user selection only
280
+ const [testPreviewDeliverySettings, setTestPreviewDeliverySettings] = useState({
281
+ [CHANNELS.SMS]: {
282
+ domainId: null, domainGatewayMapId: null, gsmSenderId: '', cdmaSenderId: '',
283
+ },
284
+ [CHANNELS.EMAIL]: {
285
+ domainId: null, domainGatewayMapId: null, senderEmail: '', senderLabel: '', senderReplyTo: '',
286
+ },
287
+ [CHANNELS.WHATSAPP]: {
288
+ domainId: null, senderMobNum: '', sourceAccountIdentifier: '',
289
+ },
290
+ [CHANNELS.RCS]: {
291
+ domainId: null,
292
+ domainGatewayMapId: null,
293
+ gsmSenderId: '',
294
+ smsFallbackDomainId: null,
295
+ cdmaSenderId: '', // gsmSenderId = RCS sender (domainId|senderId), cdmaSenderId = SMS fallback
296
+ },
297
+ });
298
+
299
+ const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
300
+ const formDataForSendTest = formData ?? (content && typeof content === 'object' && !Array.isArray(content) ? content : formData);
301
+ const smsTemplateConfigs = formDataForSendTest?.templateConfigs || {};
302
+ const smsTraiDltEnabled = !!smsTemplateConfigs?.traiDltEnabled;
303
+ const registeredSenderIds = smsTemplateConfigs?.registeredSenderIds || [];
304
+
305
+ // Fetch sender details and WeCRM accounts when Test & Preview opens (SMS, Email, WhatsApp, RCS — same process)
306
+ useEffect(() => {
307
+ if (!show || !channel) {
308
+ return;
309
+ }
310
+ if (channelsWithDeliverySettings.includes(channel)) {
311
+ if (actions.getSenderDetailsRequested) {
312
+ actions.getSenderDetailsRequested({ channel, orgUnitId: orgUnitId ?? -1 });
313
+ }
314
+ // SMS domains/senders are needed for RCS delivery UI (fallback row + slidebox) whenever RCS is open — not only when fallback body exists.
315
+ if (channel === CHANNELS.RCS && actions.getSenderDetailsRequested) {
316
+ actions.getSenderDetailsRequested({ channel: CHANNELS.SMS, orgUnitId: orgUnitId ?? -1 });
317
+ }
318
+ if (channel === CHANNELS.WHATSAPP && actions.getWeCrmAccountsRequested) {
319
+ actions.getWeCrmAccountsRequested({ sourceName: CHANNELS.WHATSAPP });
320
+ }
321
+ }
322
+ }, [show, channel, orgUnitId, actions]);
323
+
324
+ const findDefault = (arr) => (arr && arr.find((x) => x.default)) || (arr && arr[0]) || {};
325
+
326
+ // Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
327
+ useEffect(() => {
328
+ if (!channel || !channelsWithDeliverySettings.includes(channel)) return;
329
+
330
+ if (channel === CHANNELS.RCS) {
331
+ const rcsDomainRows = senderDetailsByChannel?.[CHANNELS.RCS] || [];
332
+ const smsFallbackDomainRows = senderDetailsByChannel?.[CHANNELS.SMS] || [];
333
+ if (!rcsDomainRows.length) return;
334
+
335
+ const currentRcsDeliverySettings = testPreviewDeliverySettings?.[CHANNELS.RCS] || {};
336
+ const isRcsGsmSenderUnset = !currentRcsDeliverySettings?.gsmSenderId;
337
+ const isSmsFallbackSenderUnset = !currentRcsDeliverySettings?.cdmaSenderId;
338
+ const isRcsDeliveryFullyUnset = isRcsGsmSenderUnset && isSmsFallbackSenderUnset;
339
+ const shouldOnlyFillSmsFallbackSender =
340
+ !isRcsGsmSenderUnset && isSmsFallbackSenderUnset && smsFallbackDomainRows.length > 0;
341
+
342
+ if (!isRcsDeliveryFullyUnset && !shouldOnlyFillSmsFallbackSender) return;
343
+
344
+ const firstRcsDomain = rcsDomainRows[0];
345
+ const firstSmsFallbackDomain = smsFallbackDomainRows[0];
346
+ const usableRcsGsmSenders = filterUsableGsmSendersForDomain(
347
+ firstRcsDomain,
348
+ firstRcsDomain?.gsmSenders,
349
+ { skipDomainNameEchoFilter: true },
350
+ );
351
+ const usableSmsFallbackGsmSenders = firstSmsFallbackDomain
352
+ ? filterUsableGsmSendersForDomain(firstSmsFallbackDomain, firstSmsFallbackDomain?.gsmSenders)
353
+ : [];
354
+ const defaultRcsGsmSender = usableRcsGsmSenders[0];
355
+ const defaultSmsFallbackGsmSender = usableSmsFallbackGsmSenders[0];
356
+ const rcsSenderCompositeValue =
357
+ firstRcsDomain?.domainId != null && defaultRcsGsmSender?.value != null
358
+ ? `${firstRcsDomain.domainId}|${defaultRcsGsmSender.value}`
359
+ : (defaultRcsGsmSender?.value || '');
360
+ const smsFallbackSenderCompositeValue =
361
+ firstSmsFallbackDomain?.domainId != null && defaultSmsFallbackGsmSender?.value != null
362
+ ? `${firstSmsFallbackDomain.domainId}|${defaultSmsFallbackGsmSender.value}`
363
+ : (defaultSmsFallbackGsmSender?.value || '');
364
+
365
+ setTestPreviewDeliverySettings((prev) => {
366
+ const previousRcsSettings = prev?.[CHANNELS.RCS] || {};
367
+ if (shouldOnlyFillSmsFallbackSender) {
368
+ if (!smsFallbackSenderCompositeValue) return prev;
369
+ return {
370
+ ...prev,
371
+ [CHANNELS.RCS]: {
372
+ ...previousRcsSettings,
373
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
374
+ cdmaSenderId: smsFallbackSenderCompositeValue,
375
+ },
376
+ };
377
+ }
378
+ return {
379
+ ...prev,
380
+ [CHANNELS.RCS]: {
381
+ domainId: firstRcsDomain?.domainId ?? null,
382
+ domainGatewayMapId: firstRcsDomain?.dgmId ?? null,
383
+ gsmSenderId: rcsSenderCompositeValue,
384
+ smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
385
+ cdmaSenderId: smsFallbackSenderCompositeValue,
386
+ },
387
+ };
388
+ });
389
+ return;
390
+ }
391
+
392
+ const domains = senderDetailsByChannel[channel];
393
+ if (!domains || domains.length === 0) return;
394
+ const {
395
+ domainId = '', gsmSenderId = '', cdmaSenderId = '', senderEmail = '', senderMobNum = '',
396
+ } = testPreviewDeliverySettings[channel] || {};
397
+ const isEmptySelection = !domainId && !gsmSenderId && !cdmaSenderId && !senderEmail && !senderMobNum;
398
+ if (!isEmptySelection) return;
399
+
400
+ const whatsappAccountFromForm = channel === CHANNELS.WHATSAPP ? formData?.accountName : undefined;
401
+ const matchedWhatsappAccount = whatsappAccountFromForm
402
+ ? (wecrmAccounts || []).find((account) => account?.name === whatsappAccountFromForm)
403
+ : null;
404
+ const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled && registeredSenderIds?.length
405
+ ? domains.filter((domain) => (domain?.gsmSenders || []).some((gsm) =>
406
+ registeredSenderIds?.includes(gsm?.value)))
407
+ : domains;
408
+ const [defaultDomain] = domains;
409
+ const [firstSmsDomain] = smsDomains;
410
+ let firstDomain = defaultDomain;
411
+ if (channel === CHANNELS.WHATSAPP && matchedWhatsappAccount?.sourceAccountIdentifier) {
412
+ firstDomain = domains.find((domain) => domain?.sourceAccountIdentifier === matchedWhatsappAccount.sourceAccountIdentifier) || defaultDomain;
413
+ } else if (channel === CHANNELS.SMS) {
414
+ firstDomain = firstSmsDomain;
415
+ }
416
+ if (!firstDomain) return;
417
+ setTestPreviewDeliverySettings((prev) => {
418
+ const next = { ...prev };
419
+ if (channel === CHANNELS.SMS) {
420
+ const smsGsmSenders = smsTraiDltEnabled
421
+ ? (firstDomain?.gsmSenders || []).filter((gsm) => registeredSenderIds?.includes(gsm?.value))
422
+ : firstDomain?.gsmSenders;
423
+ next[channel] = {
424
+ domainId: firstDomain.domainId,
425
+ domainGatewayMapId: firstDomain.dgmId,
426
+ gsmSenderId: findDefault(smsGsmSenders)?.value || smsGsmSenders?.[0]?.value || '',
427
+ cdmaSenderId: findDefault(firstDomain.cdmaSenders)?.value || '',
428
+ };
429
+ } else if (channel === CHANNELS.EMAIL) {
430
+ const defSender = findDefault(firstDomain.emailSenders);
431
+ const defReply = findDefault(firstDomain.emailRepliers);
432
+ next[channel] = {
433
+ domainId: firstDomain.domainId,
434
+ domainGatewayMapId: firstDomain.dgmId,
435
+ senderEmail: defSender?.value || '',
436
+ senderLabel: defSender?.label || '',
437
+ senderReplyTo: defReply?.value || '',
438
+ };
439
+ } else if (channel === CHANNELS.WHATSAPP) {
440
+ const accId = firstDomain.sourceAccountIdentifier;
441
+ next[channel] = {
442
+ domainId: firstDomain.domainId,
443
+ senderMobNum: firstDomain.gsmSenders?.[0]?.value || '',
444
+ sourceAccountIdentifier: matchedWhatsappAccount?.sourceAccountIdentifier || accId || '',
445
+ };
446
+ }
447
+ return next;
448
+ });
449
+ }, [channel, formData?.accountName, senderDetailsByChannel, smsTraiDltEnabled, registeredSenderIds, wecrmAccounts]);
149
450
 
150
451
  // ============================================
151
452
  // MEMOIZED VALUES
152
453
  // ============================================
153
454
 
455
+ const allTags = useMemo(
456
+ () => [...requiredTags, ...optionalTags, ...smsFallbackRequiredTags, ...smsFallbackOptionalTags],
457
+ [requiredTags, optionalTags, smsFallbackRequiredTags, smsFallbackOptionalTags]
458
+ );
459
+
460
+ const allRequiredTags = useMemo(
461
+ () => [...requiredTags, ...smsFallbackRequiredTags],
462
+ [requiredTags, smsFallbackRequiredTags]
463
+ );
464
+
465
+ const buildEmptyValues = useCallback(
466
+ () => allTags.reduce((acc, tag) => {
467
+ const key = tag?.fullPath;
468
+ if (key) acc[key] = '';
469
+ return acc;
470
+ }, {}),
471
+ [allTags]
472
+ );
473
+
154
474
  // Check if update preview button should be disabled
155
475
  const isUpdatePreviewDisabled = useMemo(() => (
156
- requiredTags.some((tag) => !customValues[tag.fullPath])
157
- ), [requiredTags, customValues]);
476
+ allRequiredTags.some((tag) => !customValues[tag.fullPath])
477
+ ), [allRequiredTags, customValues]);
158
478
 
159
479
  // Get current content based on channel and editor type
160
480
  const getCurrentContent = useMemo(() => {
@@ -198,6 +518,13 @@ const CommonTestAndPreview = (props) => {
198
518
  return currentTabData.base['sms-editor'];
199
519
  }
200
520
  }
521
+ // DLT / Test & Preview shape: { templateConfigs: { template, templateId, ... } }
522
+ if (formData.templateConfigs?.template) {
523
+ const smsDltTemplateValue = formData.templateConfigs.template;
524
+ if (typeof smsDltTemplateValue === 'string') return smsDltTemplateValue;
525
+ if (Array.isArray(smsDltTemplateValue)) return smsDltTemplateValue.join('');
526
+ return '';
527
+ }
201
528
  }
202
529
 
203
530
  // SMS channel fallback - if formData is not available, use content directly
@@ -243,7 +570,70 @@ const CommonTestAndPreview = (props) => {
243
570
  return content || '';
244
571
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
245
572
 
246
- // Build test entities tree data
573
+ const leftPanelExtractedTags = useMemo(() => {
574
+ if (channel === CHANNELS.SMS) {
575
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
576
+ if (!smsTemplateHasMustacheTags(smsEditorBody)) return [];
577
+ const extractTagsFromApi = extractedTags ?? [];
578
+ if (extractTagsFromApi.length > 0) return extractTagsFromApi;
579
+ return buildSyntheticSmsMustacheTags(smsEditorBody);
580
+ }
581
+ const hasFallbackSmsBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
582
+ if (channel === CHANNELS.RCS && hasFallbackSmsBody) {
583
+ const rcsPrimaryTags = extractedTags ?? [];
584
+ const fallbackSmsTextForTags = smsFallbackTextForTagExtraction ?? '';
585
+ const fallbackSmsTagRows = smsTemplateHasMustacheTags(fallbackSmsTextForTags)
586
+ ? (smsFallbackExtractedTags?.length > 0
587
+ ? smsFallbackExtractedTags
588
+ : buildSyntheticSmsMustacheTags(fallbackSmsTextForTags))
589
+ : [];
590
+ const mergedRcsAndFallbackTags = [...rcsPrimaryTags, ...fallbackSmsTagRows];
591
+ if (mergedRcsAndFallbackTags.length > 0) return mergedRcsAndFallbackTags;
592
+ return buildSyntheticSmsMustacheTags(fallbackSmsTextForTags);
593
+ }
594
+ return extractedTags ?? [];
595
+ }, [
596
+ channel,
597
+ extractedTags,
598
+ getCurrentContent,
599
+ smsFallbackContent,
600
+ smsFallbackExtractedTags,
601
+ smsFallbackTextForTagExtraction,
602
+ ]);
603
+
604
+ const isRcsSmsFallbackPreviewEnabled =
605
+ channel === CHANNELS.RCS
606
+ && !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
607
+ // Only treat as SMS when user is on the Fallback SMS tab — not whenever fallback exists (RCS tab needs RCS preview API).
608
+ const isSmsFallbackTabActive = isRcsSmsFallbackPreviewEnabled && activePreviewTab === PREVIEW_TAB_SMS_FALLBACK;
609
+ const activeChannelForActions = isSmsFallbackTabActive ? CHANNELS.SMS : channel;
610
+ // VarSegment slot values live in rcsSmsFallbackVarMapped; raw templateContent alone is stale for /preview Body.
611
+ const resolvedSmsFallbackBodyForPreviewTab =
612
+ smsFallbackTextForTagExtraction
613
+ || smsFallbackContent?.templateContent
614
+ || smsFallbackContent?.content
615
+ || '';
616
+ const activeContentForActions = isSmsFallbackTabActive
617
+ ? resolvedSmsFallbackBodyForPreviewTab
618
+ : getCurrentContent;
619
+
620
+ /**
621
+ * SMS fallback pane must show /preview API result when user updated preview on that tab (plain text).
622
+ * Skip when resolvedBody is RCS-shaped (e.g. user last previewed on RCS tab).
623
+ */
624
+ // smsFallbackPreviewText is the single source of truth for the resolved SMS fallback preview.
625
+ // It is set only by syncSmsFallbackPreview (called from handleUpdatePreview and the
626
+ // prefilled-values effect) and reset to undefined on discard / slidebox close.
627
+ // Using previewDataHtml as a fallback is unsafe because that state is shared with the primary
628
+ // RCS preview and can contain stale SMS or RCS content.
629
+ const smsFallbackResolvedText = useMemo(() => {
630
+ const hasFallbackBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
631
+ if (channel !== CHANNELS.RCS || !hasFallbackBody) return undefined;
632
+ if (smsFallbackPreviewText != null) return smsFallbackPreviewText;
633
+ return undefined;
634
+ }, [channel, smsFallbackContent, smsFallbackPreviewText]);
635
+
636
+ // Build test entities tree data from testCustomers prop
247
637
  const testEntitiesTreeData = useMemo(() => {
248
638
  const groupsNode = {
249
639
  title: 'Groups',
@@ -355,6 +745,37 @@ const CommonTestAndPreview = (props) => {
355
745
  }
356
746
  };
357
747
 
748
+ /**
749
+ * When RCS has SMS fallback, refresh fallback preview text via the same Liquid /preview API
750
+ * (separate call with SMS channel + fallback template body). Used after primary preview updates.
751
+ */
752
+ const syncSmsFallbackPreview = async (customValuesForResolve, selectedCustomerObj) => {
753
+ const fallbackBodyForLiquidPreview =
754
+ getSmsFallbackTextForTagExtraction(smsFallbackContent)
755
+ || smsFallbackContent?.templateContent
756
+ || smsFallbackContent?.content
757
+ || '';
758
+ if (channel !== CHANNELS.RCS || !String(fallbackBodyForLiquidPreview).trim()) return;
759
+ try {
760
+ const smsFallbackPayload = preparePreviewPayload(
761
+ CHANNELS.SMS,
762
+ formData || {},
763
+ fallbackBodyForLiquidPreview,
764
+ customValuesForResolve,
765
+ selectedCustomerObj
766
+ );
767
+ const fallbackResponse = await Api.updateEmailPreview(smsFallbackPayload);
768
+ const fallbackPreview = extractPreviewFromLiquidResponse(fallbackResponse);
769
+ setSmsFallbackPreviewText(
770
+ typeof fallbackPreview?.resolvedBody === 'string'
771
+ ? fallbackPreview.resolvedBody
772
+ : undefined
773
+ );
774
+ } catch (e) {
775
+ /* keep existing smsFallbackPreviewText on failure */
776
+ }
777
+ };
778
+
358
779
  /**
359
780
  * Prepare payload for tag extraction based on channel
360
781
  */
@@ -442,7 +863,175 @@ const CommonTestAndPreview = (props) => {
442
863
  * Prepare payload for test message sending based on channel
443
864
  * Updated to match API structure with ouId, sourceEntityId, module, deliverySettings, etc.
444
865
  */
445
- const prepareTestMessagePayload = (channelType, formDataObj, contentStr, customValuesObj, recipientDetails, previewDataObj) => {
866
+
867
+ const getCarouselMappedData = (carouselData = []) => carouselData.map((carousel) => {
868
+ const {
869
+ bodyText, imageUrl, videoUrl, videoPreviewImg, buttons, mediaType, cardVarMapped, bodyTemplate,
870
+ } = carousel || {};
871
+ const buttonData = buttons.map((button, index) => {
872
+ const {
873
+ type, text, phone_number: phoneNumber, urlType, url,
874
+ } = button || {};
875
+ const buttonObj = {
876
+ type,
877
+ text,
878
+ index,
879
+ };
880
+ if (type === PHONE_NUMBER) {
881
+ buttonObj.phoneNumber = phoneNumber;
882
+ }
883
+ if (type === URL) {
884
+ buttonObj.url = url;
885
+ if (urlType === DYNAMIC_URL) {
886
+ const dynamicUrlPayload = url?.match(/{{(.*?)}}/g);
887
+ buttonObj.dynamicUrlPayload = dynamicUrlPayload?.length === 1 ? dynamicUrlPayload[0] : '';
888
+ }
889
+ }
890
+ return buttonObj;
891
+ });
892
+ return {
893
+ body: bodyText,
894
+ cardVarMapped,
895
+ bodyTemplate,
896
+ media: {
897
+ ...(mediaType?.toLowerCase() === IMAGE.toLowerCase() && {
898
+ url: imageUrl,
899
+ }),
900
+ ...(mediaType?.toLowerCase() === VIDEO.toLowerCase() && {
901
+ url: videoUrl,
902
+ previewUrl: videoPreviewImg,
903
+ }),
904
+ },
905
+ buttons: buttonData,
906
+ mediaType: mediaType?.toUpperCase(),
907
+ };
908
+ });
909
+
910
+ /**
911
+ * Build createMessageMeta payload for RCS (test message).
912
+ * rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
913
+ * Then rcsDeliverySettings, executionParams, clientName last.
914
+ */
915
+ const buildRcsTestMessagePayload = (formDataObj, _contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra = {}) => {
916
+ const plainCustom =
917
+ customValuesObj != null && typeof customValuesObj.toJS === 'function'
918
+ ? customValuesObj.toJS()
919
+ : (customValuesObj || {});
920
+ const userVarMap = Object.fromEntries(
921
+ Object.entries(plainCustom).filter(([, v]) => v != null && String(v).trim() !== '')
922
+ );
923
+ const rcsData = formDataObj?.versions?.base?.content?.RCS ?? formDataObj?.content?.RCS ?? {};
924
+ const rcsContent = rcsData?.rcsContent || {};
925
+ const smsFallback = rcsData?.smsFallBackContent || {};
926
+ let cardContentList = [];
927
+ if (Array.isArray(rcsContent?.cardContent)) {
928
+ cardContentList = rcsContent.cardContent;
929
+ } else if (rcsContent?.cardContent) {
930
+ cardContentList = [rcsContent.cardContent];
931
+ }
932
+ // Merge test customValues into cardVarMapped (template snapshot + user-entered RCS + fallback SMS tags).
933
+ const cardContent = cardContentList.map((rcsCard) => {
934
+ const baseMap = rcsCard.cardVarMapped || {};
935
+ const mergedCardVarMapped =
936
+ Object.keys(userVarMap).length > 0 ? { ...baseMap, ...userVarMap } : baseMap;
937
+ const mediaNorm = rcsCard?.media ? normalizeRcsTestCardMedia(rcsCard.media) : undefined;
938
+ const suggestionsRaw = Array.isArray(rcsCard?.suggestions) ? rcsCard.suggestions : [];
939
+ const suggestionsMapped = suggestionsRaw.map((suggestionItem, i) =>
940
+ mapRcsSuggestionForTestMeta(suggestionItem, i));
941
+ return {
942
+ title: rcsCard?.title ?? '',
943
+ description: rcsCard?.description ?? '',
944
+ mediaType: rcsCard?.mediaType ?? MEDIA_TYPE_TEXT,
945
+ ...(mediaNorm && { media: mediaNorm }),
946
+ ...(Object.keys(mergedCardVarMapped).length > 0 && { cardVarMapped: mergedCardVarMapped }),
947
+ ...(suggestionsMapped.length > 0 && { suggestions: suggestionsMapped }),
948
+ };
949
+ });
950
+ // Prefer parent `smsFallbackContent` snapshot (rcsExtra) — includes VarSegment-resolved body via
951
+ // getSmsFallbackTextForTagExtraction. Nested formData.smsFallBackContent is often stale vs live editor.
952
+ const rcsExtraFallbackTemplate = rcsExtra?.smsFallbackTemplateContent;
953
+ const hasResolvedFallbackBodyFromRcsExtra =
954
+ rcsExtraFallbackTemplate != null
955
+ && String(rcsExtraFallbackTemplate).trim() !== '';
956
+ const smsMessageRaw = hasResolvedFallbackBodyFromRcsExtra
957
+ ? String(rcsExtraFallbackTemplate)
958
+ : (smsFallback?.smsContent ?? smsFallback?.message ?? '');
959
+ const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
960
+ ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
961
+ : deliverySettingsOverride?.cdmaSenderId;
962
+ const deliveryFallbackSmsId =
963
+ typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
964
+ const creativeFallbackSmsId =
965
+ smsFallback?.senderId != null ? String(smsFallback.senderId).trim() : '';
966
+ const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
967
+
968
+ const smsFallBackContent =
969
+ smsMessageRaw.trim() !== ''
970
+ ? { message: smsMessageRaw }
971
+ : undefined;
972
+
973
+ // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
974
+ const accountIdForMeta =
975
+ rcsContent?.accountId != null && String(rcsContent.accountId).trim() !== ''
976
+ ? String(rcsContent.accountId)
977
+ : undefined;
978
+
979
+ const rcsRichCardContent = {
980
+ contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
981
+ cardType: rcsContent?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
982
+ cardSettings: rcsContent?.cardSettings ?? {
983
+ cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
984
+ cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
985
+ },
986
+ ...(cardContent.length > 0 && { cardContent }),
987
+ };
988
+
989
+ const rcsMessageContent = {
990
+ channel: CHANNELS.RCS,
991
+ ...(accountIdForMeta && { accountId: accountIdForMeta }),
992
+ rcsRichCardContent,
993
+ ...(smsFallBackContent && { smsFallBackContent }),
994
+ };
995
+ const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
996
+ const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
997
+ const rcsDeliverySettings = {
998
+ channelSettings: {
999
+ channel: CHANNELS.RCS,
1000
+ rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
1001
+ domainId:
1002
+ rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
1003
+ ? Number(rcsDomainId)
1004
+ : (deliverySettingsOverride?.domainId ?? 0),
1005
+ fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
1006
+ },
1007
+ additionalSettings: {
1008
+ useTinyUrl: false,
1009
+ encryptUrl: false,
1010
+ linkTrackingEnabled: false,
1011
+ bypassControlUser: false,
1012
+ userSubscriptionDisabled: false,
1013
+ },
1014
+ };
1015
+ const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
1016
+ return {
1017
+ ...restBase,
1018
+ rcsMessageContent,
1019
+ rcsDeliverySettings,
1020
+ executionParams: {},
1021
+ clientName: baseClientName,
1022
+ };
1023
+ };
1024
+
1025
+ const prepareTestMessagePayload = (
1026
+ channelType,
1027
+ formDataObj,
1028
+ contentStr,
1029
+ customValuesObj,
1030
+ recipientDetails,
1031
+ previewDataObj,
1032
+ deliverySettingsOverride,
1033
+ rcsExtra = {},
1034
+ ) => {
446
1035
  // Base payload structure common to all channels
447
1036
  const basePayload = {
448
1037
  ouId: -1,
@@ -468,6 +1057,14 @@ const CommonTestAndPreview = (props) => {
468
1057
  ? resolveTagsInText(subject, customValuesObj)
469
1058
  : subject;
470
1059
 
1060
+ const emailChannelSettings = {
1061
+ channel: CHANNELS.EMAIL,
1062
+ senderLabel: deliverySettingsOverride?.senderLabel ?? '',
1063
+ senderId: deliverySettingsOverride?.senderEmail ?? '',
1064
+ replyToId: deliverySettingsOverride?.senderReplyTo ?? '',
1065
+ domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
1066
+ domainId: deliverySettingsOverride?.domainId ?? '',
1067
+ };
471
1068
  return {
472
1069
  ...basePayload,
473
1070
  emailDeliverySettings: {
@@ -478,12 +1075,7 @@ const CommonTestAndPreview = (props) => {
478
1075
  skipRateLimit: false,
479
1076
  bypassControlUser: false,
480
1077
  },
481
- channelSettings: {
482
- channel: CHANNELS.EMAIL,
483
- senderLabel: '',
484
- senderId: '',
485
- replyToId: '',
486
- },
1078
+ channelSettings: emailChannelSettings,
487
1079
  },
488
1080
  emailMessageContent: {
489
1081
  channel: CHANNELS.EMAIL,
@@ -520,8 +1112,12 @@ const CommonTestAndPreview = (props) => {
520
1112
  },
521
1113
  smsDeliverySettings: {
522
1114
  channelSettings: {
523
- gsmSenderId: '',
524
- domainId: null,
1115
+ channel: CHANNELS.SMS,
1116
+ gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
1117
+ domainId: deliverySettingsOverride?.domainId ?? null,
1118
+ domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
1119
+ targetNdnc: false,
1120
+ cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
525
1121
  },
526
1122
  additionalSettings: {
527
1123
  useTinyUrl: false,
@@ -653,20 +1249,21 @@ const CommonTestAndPreview = (props) => {
653
1249
 
654
1250
  // Add carousel data if mediaType is CAROUSEL
655
1251
  if (mediaType === MEDIA_TYPE_CAROUSEL && formDataObj?.carouselData) {
656
- templateConfigs.cards = formDataObj.carouselData;
1252
+ templateConfigs.cards = getCarouselMappedData(formDataObj.carouselData);
657
1253
  templateConfigs.mediaType = formDataObj?.carouselMediaType?.toUpperCase() || MEDIA_TYPE_IMAGE;
658
1254
  }
659
1255
 
660
- // Extract delivery settings
661
- const senderMobNum = formDataObj?.senderMobNum || additionalProps?.senderMobNum || '';
662
- const domainId = formDataObj?.domainId || additionalProps?.domainId || null;
1256
+ // Extract delivery settings (override from Test & Preview delivery settings when present)
1257
+ const senderMobNum = deliverySettingsOverride?.senderMobNum ?? formDataObj?.senderMobNum ?? additionalProps?.senderMobNum ?? '';
1258
+ const domainId = deliverySettingsOverride?.domainId ?? formDataObj?.domainId ?? additionalProps?.domainId ?? null;
1259
+ const sourceAccountIdentifier = deliverySettingsOverride?.sourceAccountIdentifier ?? formDataObj?.sourceAccountIdentifier ?? '';
663
1260
 
664
1261
  return {
665
1262
  ...basePayload,
666
1263
  whatsappMessageContent: {
667
- messageBody: templateEditorValue || '',
1264
+ messageBody: resolvedMessageBody || templateEditorValue || '',
668
1265
  accountId: formDataObj?.accountId || '',
669
- sourceAccountIdentifier: formDataObj?.sourceAccountIdentifier || '',
1266
+ sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
670
1267
  accountName: formDataObj?.accountName || '',
671
1268
  templateConfigs,
672
1269
  channel: CHANNELS.WHATSAPP,
@@ -689,16 +1286,7 @@ const CommonTestAndPreview = (props) => {
689
1286
  }
690
1287
 
691
1288
  case CHANNELS.RCS:
692
- return {
693
- ...basePayload,
694
- rcsMessageContent: {
695
- channel: CHANNELS.RCS,
696
- messageBody: contentStr,
697
- rcsType: additionalProps?.rcsType,
698
- rcsImageSrc: formDataObj?.rcsImageSrc,
699
- rcsSuggestions: formDataObj?.rcsSuggestions,
700
- },
701
- };
1289
+ return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
702
1290
 
703
1291
  case CHANNELS.INAPP: {
704
1292
  // InApp payload structure similar to MobilePush
@@ -1800,6 +2388,10 @@ const CommonTestAndPreview = (props) => {
1800
2388
  formatMessage,
1801
2389
  lastModified: formData?.lastModified,
1802
2390
  updatedByName: formData?.updatedByName,
2391
+ smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
2392
+ smsFallbackResolvedText,
2393
+ activePreviewTab,
2394
+ onPreviewTabChange: setActivePreviewTab,
1803
2395
  };
1804
2396
  };
1805
2397
 
@@ -1839,7 +2431,12 @@ const CommonTestAndPreview = (props) => {
1839
2431
  }, [show, beeInstance, currentTab, channel]);
1840
2432
 
1841
2433
  /**
1842
- * Initial data load when slidebox opens
2434
+ * Initial data load when slidebox opens.
2435
+ * EXTRACT TAGS CALL SITES (on open/edit RCS):
2436
+ * 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
2437
+ * 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
2438
+ * 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
2439
+ * The "Process extracted tags" effect only processes API results and must not call extract again.
1843
2440
  */
1844
2441
  useEffect(() => {
1845
2442
  if (show) {
@@ -1924,7 +2521,61 @@ const CommonTestAndPreview = (props) => {
1924
2521
  actions.getTestGroupsRequested();
1925
2522
  }
1926
2523
  }
1927
- }, [show, beeInstance, currentTab, channel]);
2524
+ // getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
2525
+ }, [show, beeInstance, currentTab, channel, getCurrentContent]);
2526
+
2527
+ /**
2528
+ * RCS with SMS fallback: extract tags for fallback SMS content as well
2529
+ * (so we can show a separate "Fallback SMS tags" section in left panel).
2530
+ */
2531
+ useEffect(() => {
2532
+ let cancelled = false;
2533
+
2534
+ if (!show || channel !== CHANNELS.RCS) {
2535
+ return () => {
2536
+ cancelled = true;
2537
+ };
2538
+ }
2539
+
2540
+ if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
2541
+ setSmsFallbackExtractedTags([]);
2542
+ setSmsFallbackRequiredTags([]);
2543
+ setSmsFallbackOptionalTags([]);
2544
+ return () => {
2545
+ cancelled = true;
2546
+ };
2547
+ }
2548
+
2549
+ setIsExtractingSmsFallbackTags(true);
2550
+ (async () => {
2551
+ try {
2552
+ const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
2553
+ const payload = {
2554
+ messageTitle: '',
2555
+ messageBody: fallbackBodyForExtractApi || '',
2556
+ };
2557
+ const response = await Api.extractTagsWithMetaData(payload); //not using saga action here because we dont store fallbacksms related data in store but only in useState since this is only used in RCS SMS fallback
2558
+ let smsFallbackTagTree = response?.data ?? [];
2559
+ if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
2560
+ if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
2561
+ smsFallbackTagTree = [];
2562
+ } else if (smsFallbackTagTree.length === 0) {
2563
+ smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
2564
+ }
2565
+ if (cancelled) return;
2566
+ setSmsFallbackExtractedTags(smsFallbackTagTree);
2567
+ } catch (e) {
2568
+ if (cancelled) return;
2569
+ setSmsFallbackExtractedTags([]);
2570
+ } finally {
2571
+ if (!cancelled) setIsExtractingSmsFallbackTags(false);
2572
+ }
2573
+ })();
2574
+
2575
+ return () => {
2576
+ cancelled = true;
2577
+ };
2578
+ }, [show, channel, smsFallbackContent]);
1928
2579
 
1929
2580
  /**
1930
2581
  * Email-specific: Handle content updates for both BEE and CKEditor
@@ -1976,15 +2627,22 @@ const CommonTestAndPreview = (props) => {
1976
2627
  setSelectedCustomer(null);
1977
2628
  setRequiredTags([]);
1978
2629
  setOptionalTags([]);
2630
+ setSmsFallbackExtractedTags([]);
2631
+ setSmsFallbackRequiredTags([]);
2632
+ setSmsFallbackOptionalTags([]);
2633
+ setIsExtractingSmsFallbackTags(false);
1979
2634
  setCustomValues({});
1980
2635
  setShowJSON(false);
1981
2636
  setTagsExtracted(false);
1982
2637
  setPreviewDevice(DESKTOP);
2638
+ setActivePreviewTab(PREVIEW_TAB_RCS);
2639
+ setSmsFallbackPreviewText(undefined);
1983
2640
  setSelectedTestEntities([]);
1984
2641
  actions.clearPrefilledValues();
1985
2642
  } else {
1986
2643
  // Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
1987
2644
  setPreviewDevice(initialDevice);
2645
+ setActivePreviewTab(PREVIEW_TAB_RCS);
1988
2646
  }
1989
2647
  }, [show, initialDevice]);
1990
2648
 
@@ -1993,79 +2651,10 @@ const CommonTestAndPreview = (props) => {
1993
2651
  */
1994
2652
  useEffect(() => {
1995
2653
  if (previewData) {
1996
- setPreviewDataHtml(previewData);
2654
+ setPreviewDataHtml(toPlainPreviewData(previewData));
1997
2655
  }
1998
2656
  }, [previewData]);
1999
2657
 
2000
- /**
2001
- * Process extracted tags and categorize them
2002
- */
2003
- useEffect(() => {
2004
- // Categorize tags into required and optional
2005
- const required = [];
2006
- const optional = [];
2007
- let hasPersonalizationTags = false;
2008
-
2009
- if (extractedTags?.length > 0) {
2010
- const processTag = (tag, parentPath = '') => {
2011
- const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2012
-
2013
- // Skip unsubscribe tag for input fields
2014
- if (tag?.name === UNSUBSCRIBE_TAG_NAME) {
2015
- return;
2016
- }
2017
-
2018
- hasPersonalizationTags = true;
2019
-
2020
- if (tag?.metaData?.userDriven === false) {
2021
- required.push({
2022
- ...tag,
2023
- fullPath: currentPath,
2024
- });
2025
- } else if (tag?.metaData?.userDriven === true) {
2026
- optional.push({
2027
- ...tag,
2028
- fullPath: currentPath,
2029
- });
2030
- }
2031
-
2032
- if (tag?.children?.length > 0) {
2033
- tag.children.forEach((child) => processTag(child, currentPath));
2034
- }
2035
- };
2036
-
2037
- extractedTags.forEach((tag) => processTag(tag));
2038
-
2039
- if (hasPersonalizationTags) {
2040
- setRequiredTags(required);
2041
- setOptionalTags(optional);
2042
- setTagsExtracted(true); // Mark tags as extracted and processed
2043
-
2044
- // Initialize custom values for required tags
2045
- const initialValues = {};
2046
- required.forEach((tag) => {
2047
- initialValues[tag?.fullPath] = '';
2048
- });
2049
- optional.forEach((tag) => {
2050
- initialValues[tag?.fullPath] = '';
2051
- });
2052
- setCustomValues(initialValues);
2053
- } else {
2054
- // Reset all tag-related state if no personalization tags
2055
- setRequiredTags([]);
2056
- setOptionalTags([]);
2057
- setCustomValues({});
2058
- setTagsExtracted(false);
2059
- }
2060
- } else {
2061
- // Reset all tag-related state if no tags
2062
- setRequiredTags([]);
2063
- setOptionalTags([]);
2064
- setCustomValues({});
2065
- setTagsExtracted(false);
2066
- }
2067
- }, [extractedTags]);
2068
-
2069
2658
  /**
2070
2659
  * Handle customer selection and fetch prefilled values
2071
2660
  */
@@ -2073,17 +2662,15 @@ const CommonTestAndPreview = (props) => {
2073
2662
  if (selectedCustomer && config.enableCustomerSearch !== false) {
2074
2663
  setTagsExtracted(true); // Auto-open custom values editor
2075
2664
 
2076
- // Get all available tags
2077
- const allTags = [...requiredTags, ...optionalTags];
2078
2665
  const requiredTagObj = {};
2079
- requiredTags.forEach((tag) => {
2666
+ allRequiredTags.forEach((tag) => {
2080
2667
  requiredTagObj[tag?.fullPath] = '';
2081
2668
  });
2082
2669
  if (allTags.length > 0) {
2083
2670
  const payload = preparePreviewPayload(
2084
- channel,
2671
+ activeChannelForActions,
2085
2672
  formData || {},
2086
- getCurrentContent,
2673
+ activeContentForActions,
2087
2674
  {
2088
2675
  ...requiredTagObj,
2089
2676
  },
@@ -2092,7 +2679,7 @@ const CommonTestAndPreview = (props) => {
2092
2679
  actions.getPrefilledValuesRequested(payload);
2093
2680
  }
2094
2681
  }
2095
- }, [selectedCustomer]);
2682
+ }, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
2096
2683
 
2097
2684
  /**
2098
2685
  * Update custom values with prefilled values from API
@@ -2103,7 +2690,7 @@ const CommonTestAndPreview = (props) => {
2103
2690
  if (prefilledValues && selectedCustomer) {
2104
2691
  // Always replace all values with prefilled values
2105
2692
  const updatedValues = {};
2106
- [...requiredTags, ...optionalTags].forEach((tag) => {
2693
+ allTags.forEach((tag) => {
2107
2694
  updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
2108
2695
  });
2109
2696
 
@@ -2111,16 +2698,17 @@ const CommonTestAndPreview = (props) => {
2111
2698
 
2112
2699
  // Update preview with prefilled values (this is a valid preview call trigger)
2113
2700
  const payload = preparePreviewPayload(
2114
- channel,
2701
+ activeChannelForActions,
2115
2702
  formData || {},
2116
- getCurrentContent,
2703
+ activeContentForActions,
2117
2704
  updatedValues,
2118
2705
  selectedCustomer
2119
2706
  );
2120
2707
  actions.updatePreviewRequested(payload);
2121
2708
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2709
+ void syncSmsFallbackPreview(updatedValues, selectedCustomer);
2122
2710
  }
2123
- }, [JSON.stringify(prefilledValues), selectedCustomer]);
2711
+ }, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
2124
2712
 
2125
2713
  /**
2126
2714
  * Map channel constants to display names (lowercase for message)
@@ -2213,11 +2801,7 @@ const CommonTestAndPreview = (props) => {
2213
2801
  setTagsExtracted(true); // Auto-open custom values editor
2214
2802
 
2215
2803
  // Clear any existing values while waiting for prefilled values
2216
- const emptyValues = {};
2217
- [...requiredTags, ...optionalTags].forEach((tag) => {
2218
- emptyValues[tag?.fullPath] = '';
2219
- });
2220
- setCustomValues(emptyValues);
2804
+ setCustomValues(buildEmptyValues());
2221
2805
  };
2222
2806
 
2223
2807
  /**
@@ -2231,11 +2815,7 @@ const CommonTestAndPreview = (props) => {
2231
2815
  actions.clearPreviewErrors();
2232
2816
 
2233
2817
  // Initialize empty values for all tags
2234
- const emptyValues = {};
2235
- [...requiredTags, ...optionalTags].forEach((tag) => {
2236
- emptyValues[tag?.fullPath] = '';
2237
- });
2238
- setCustomValues(emptyValues);
2818
+ setCustomValues(buildEmptyValues());
2239
2819
 
2240
2820
  // Don't make preview call when clearing selection - just reset to raw content
2241
2821
  // Preview will be shown using raw formData/content
@@ -2271,17 +2851,17 @@ const CommonTestAndPreview = (props) => {
2271
2851
  */
2272
2852
  const handleDiscardCustomValues = () => {
2273
2853
  // Initialize empty values for all tags
2274
- const emptyValues = {};
2275
- [...requiredTags, ...optionalTags].forEach((tag) => {
2276
- emptyValues[tag?.fullPath] = '';
2277
- });
2854
+ const emptyValues = buildEmptyValues();
2278
2855
  setCustomValues(emptyValues);
2279
2856
 
2857
+ // Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
2858
+ setSmsFallbackPreviewText(undefined);
2859
+
2280
2860
  // Update preview with empty values (this is a valid preview call trigger)
2281
2861
  const payload = preparePreviewPayload(
2282
- channel,
2862
+ activeChannelForActions,
2283
2863
  formData || {},
2284
- getCurrentContent,
2864
+ activeContentForActions,
2285
2865
  emptyValues,
2286
2866
  selectedCustomer
2287
2867
  );
@@ -2296,13 +2876,15 @@ const CommonTestAndPreview = (props) => {
2296
2876
  const handleUpdatePreview = async () => {
2297
2877
  try {
2298
2878
  const payload = preparePreviewPayload(
2299
- channel,
2879
+ activeChannelForActions,
2300
2880
  formData || {},
2301
- getCurrentContent,
2881
+ activeContentForActions,
2302
2882
  customValues,
2303
2883
  selectedCustomer
2304
2884
  );
2305
2885
  await actions.updatePreviewRequested(payload);
2886
+
2887
+ await syncSmsFallbackPreview(customValues, selectedCustomer);
2306
2888
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
2307
2889
  } catch (error) {
2308
2890
  CapNotification.error({
@@ -2312,25 +2894,115 @@ const CommonTestAndPreview = (props) => {
2312
2894
  };
2313
2895
 
2314
2896
  /**
2315
- * Handle extract tags
2897
+ * Categorize extracted tags into required/optional.
2316
2898
  */
2317
- const handleExtractTags = () => {
2318
- // Get content based on channel
2319
- let contentToExtract = getCurrentContent;
2899
+ const categorizeTags = (tagsTree = []) => {
2900
+ const required = [];
2901
+ const optional = [];
2902
+ let hasPersonalizationTags = false;
2903
+ const processTag = (tag, parentPath = '') => {
2904
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
2905
+
2906
+ // Skip unsubscribe tag for input fields
2907
+ if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
2908
+
2909
+ hasPersonalizationTags = true;
2910
+ const userDriven = tag?.metaData?.userDriven;
2911
+ if (userDriven === true) {
2912
+ optional.push({ ...tag, fullPath: currentPath });
2913
+ } else {
2914
+ // false or missing (SMS/DLT extract often omits metaData) → required for test values
2915
+ required.push({ ...tag, fullPath: currentPath });
2916
+ }
2917
+
2918
+ if (tag?.children?.length > 0) {
2919
+ tag.children.forEach((child) => processTag(child, currentPath));
2920
+ }
2921
+ };
2922
+
2923
+ (tagsTree || []).forEach((tag) => processTag(tag));
2924
+ return { required, optional, hasPersonalizationTags };
2925
+ };
2926
+
2927
+ /**
2928
+ * Apply tag extraction when content comes from RCS + SMS fallback (no API call).
2929
+ */
2930
+ const applyRcsSmsFallbackTagExtraction = () => {
2931
+ const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
2932
+ const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
2933
+ let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
2934
+ ? smsFallbackExtractedTags
2935
+ : buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
2936
+ if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
2937
+ fallbackSmsTagTree = [];
2938
+ }
2939
+ const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
2940
+ setRequiredTags(rcsPrimaryCategorized.required);
2941
+ setOptionalTags(rcsPrimaryCategorized.optional);
2942
+ setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
2943
+ setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
2944
+ setTagsExtracted(
2945
+ rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
2946
+ );
2947
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
2948
+ };
2949
+
2950
+ /**
2951
+ * When extract-tags API returns, map Redux `extractedTags` into required/optional so
2952
+ * CustomValuesEditor shows personalization fields (effect was previously commented out).
2953
+ * RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
2954
+ */
2955
+ useEffect(() => {
2956
+ if (!show) return;
2957
+ const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
2958
+ if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
2959
+ applyRcsSmsFallbackTagExtraction();
2960
+ return;
2961
+ }
2962
+ const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
2963
+ let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
2964
+ ? buildSyntheticSmsMustacheTags(smsEditorBody)
2965
+ : (extractedTags ?? []);
2966
+ if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
2967
+ smsTagSource = [];
2968
+ }
2969
+ const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
2970
+ setRequiredTags(required);
2971
+ setOptionalTags(optional);
2972
+ setTagsExtracted(hasPersonalizationTags);
2973
+ setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
2974
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
2975
+ }, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
2320
2976
 
2977
+ /**
2978
+ * Get content to run tag extraction on (channel-specific).
2979
+ */
2980
+ const getContentForTagExtraction = () => {
2981
+ let contentToExtract = activeContentForActions;
2321
2982
  if (channel === CHANNELS.EMAIL && formData) {
2322
2983
  const currentTabData = formData[currentTab - 1];
2323
2984
  const activeTab = currentTabData?.activeTab;
2324
2985
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2325
2986
  contentToExtract = templateContent || contentToExtract;
2326
2987
  }
2988
+ return contentToExtract;
2989
+ };
2327
2990
 
2328
- // Check for personalization tags (excluding unsubscribe)
2991
+ /**
2992
+ * Handle extract tags
2993
+ */
2994
+ const handleExtractTags = () => {
2995
+ if (channel === CHANNELS.RCS) {
2996
+ applyRcsSmsFallbackTagExtraction();
2997
+ return;
2998
+ }
2999
+
3000
+ const contentToExtract = getContentForTagExtraction();
2329
3001
  const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
2330
3002
  const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
3003
+ const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
2331
3004
 
2332
- if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME)) {
2333
- // If only unsubscribe tag is present, show noTagsExtracted message
3005
+ if (onlyUnsubscribe) {
2334
3006
  setTagsExtracted(false);
2335
3007
  setRequiredTags([]);
2336
3008
  setOptionalTags([]);
@@ -2338,10 +3010,9 @@ const CommonTestAndPreview = (props) => {
2338
3010
  return;
2339
3011
  }
2340
3012
 
2341
- // Extract tags
2342
3013
  setTagsExtracted(true);
2343
3014
  const { templateSubject, templateContent } = prepareTagExtractionPayload(
2344
- channel,
3015
+ activeChannelForActions,
2345
3016
  formData || {},
2346
3017
  contentToExtract
2347
3018
  );
@@ -2361,7 +3032,7 @@ const CommonTestAndPreview = (props) => {
2361
3032
  const handleSendTestMessage = () => {
2362
3033
  const allUserIds = [];
2363
3034
  selectedTestEntities.forEach((entityId) => {
2364
- const group = testGroups.find((g) => g.groupId === entityId);
3035
+ const group = testGroups.find((testGroup) => testGroup.groupId === entityId);
2365
3036
  if (group) {
2366
3037
  allUserIds.push(...group.userIds);
2367
3038
  } else {
@@ -2370,14 +3041,29 @@ const CommonTestAndPreview = (props) => {
2370
3041
  });
2371
3042
  const uniqueUserIds = [...new Set(allUserIds)];
2372
3043
 
2373
- // Create initial payload based on channel
3044
+ const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
3045
+ ? testPreviewDeliverySettings[channel]
3046
+ : null;
3047
+
3048
+ // createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
3049
+ // Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
2374
3050
  const initialPayload = prepareTestMessagePayload(
2375
3051
  channel,
2376
3052
  formData || content || {},
2377
3053
  getCurrentContent,
2378
3054
  customValues,
2379
3055
  uniqueUserIds,
2380
- previewData
3056
+ previewData,
3057
+ deliveryOverride,
3058
+ channel === CHANNELS.RCS
3059
+ ? {
3060
+ smsFallbackTemplateContent:
3061
+ smsFallbackTextForTagExtraction
3062
+ || smsFallbackContent?.templateContent
3063
+ || smsFallbackContent?.content
3064
+ || '',
3065
+ }
3066
+ : {},
2381
3067
  );
2382
3068
 
2383
3069
  actions.createMessageMetaRequested(
@@ -2410,11 +3096,10 @@ const CommonTestAndPreview = (props) => {
2410
3096
  // ============================================
2411
3097
  // RENDER HELPER FUNCTIONS
2412
3098
  // ============================================
2413
-
2414
3099
  const renderLeftPanelContent = () => (
2415
3100
  <LeftPanelContent
2416
- isExtractingTags={isExtractingTags}
2417
- extractedTags={extractedTags}
3101
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
3102
+ extractedTags={leftPanelExtractedTags}
2418
3103
  selectedCustomer={selectedCustomer}
2419
3104
  handleCustomerSelect={handleCustomerSelect}
2420
3105
  handleSearchCustomer={handleSearchCustomer}
@@ -2432,15 +3117,29 @@ const CommonTestAndPreview = (props) => {
2432
3117
 
2433
3118
  const renderCustomValuesEditor = () => (
2434
3119
  <CustomValuesEditor
2435
- isExtractingTags={isExtractingTags}
3120
+ isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
2436
3121
  isUpdatePreviewDisabled={isUpdatePreviewDisabled}
2437
3122
  showJSON={showJSON}
2438
3123
  setShowJSON={setShowJSON}
2439
3124
  customValues={customValues}
2440
3125
  handleJSONTextChange={handleJSONTextChange}
2441
- extractedTags={extractedTags}
2442
- requiredTags={requiredTags}
2443
- optionalTags={optionalTags}
3126
+ sections={[
3127
+ {
3128
+ key: channel,
3129
+ title:
3130
+ channel === CHANNELS.RCS
3131
+ ? messages.rcsTagsSectionTitle
3132
+ : messages[`${channel}TagsSectionTitle`],
3133
+ requiredTags,
3134
+ optionalTags,
3135
+ },
3136
+ {
3137
+ key: PREVIEW_TAB_SMS_FALLBACK,
3138
+ title: channel === CHANNELS.RCS && smsFallbackContent?.templateContent ? messages.smsFallbackTagsSectionTitle : null,
3139
+ requiredTags: smsFallbackRequiredTags,
3140
+ optionalTags: smsFallbackOptionalTags,
3141
+ },
3142
+ ]}
2444
3143
  handleCustomValueChange={handleCustomValueChange}
2445
3144
  handleDiscardCustomValues={handleDiscardCustomValues}
2446
3145
  handleUpdatePreview={handleUpdatePreview}
@@ -2449,6 +3148,13 @@ const CommonTestAndPreview = (props) => {
2449
3148
  />
2450
3149
  );
2451
3150
 
3151
+ const handleSaveDeliverySettings = (values) => {
3152
+ setTestPreviewDeliverySettings((prev) => ({
3153
+ ...prev,
3154
+ [channel]: values,
3155
+ }));
3156
+ };
3157
+
2452
3158
  const renderSendTestMessage = () => (
2453
3159
  <SendTestMessage
2454
3160
  isFetchingTestCustomers={isFetchingTestCustomers}
@@ -2457,11 +3163,19 @@ const CommonTestAndPreview = (props) => {
2457
3163
  handleTestEntitiesChange={handleTestEntitiesChange}
2458
3164
  selectedTestEntities={selectedTestEntities}
2459
3165
  handleSendTestMessage={handleSendTestMessage}
2460
- formData={formData}
3166
+ formData={formDataForSendTest}
2461
3167
  content={getCurrentContent}
2462
3168
  channel={channel}
2463
3169
  isSendingTestMessage={isSendingTestMessage}
2464
3170
  formatMessage={formatMessage}
3171
+ deliverySettings={testPreviewDeliverySettings[channel]}
3172
+ senderDetailsByChannel={senderDetailsByChannel}
3173
+ wecrmAccounts={wecrmAccounts}
3174
+ onSaveDeliverySettings={handleSaveDeliverySettings}
3175
+ isLoadingSenderDetails={isLoadingSenderDetails}
3176
+ smsTraiDltEnabled={smsTraiDltEnabled}
3177
+ registeredSenderIds={registeredSenderIds}
3178
+ isChannelSmsFallbackPreviewEnabled={channel === CHANNELS.RCS && !!smsFallbackContent?.templateContent}
2465
3179
  />
2466
3180
  );
2467
3181
 
@@ -2562,6 +3276,10 @@ CommonTestAndPreview.propTypes = {
2562
3276
  })),
2563
3277
  isSendingTestMessage: PropTypes.bool.isRequired,
2564
3278
  intl: PropTypes.object.isRequired,
3279
+ senderDetailsByChannel: PropTypes.object,
3280
+ wecrmAccounts: PropTypes.array,
3281
+ isLoadingSenderDetails: PropTypes.bool,
3282
+ orgUnitId: PropTypes.number,
2565
3283
 
2566
3284
  // Email-specific props
2567
3285
  beeInstance: PropTypes.object,
@@ -2597,6 +3315,10 @@ CommonTestAndPreview.defaultProps = {
2597
3315
  rcsOrientation: null,
2598
3316
  rcsIosPreview: false,
2599
3317
  templateLayoutType: null,
3318
+ senderDetailsByChannel: {},
3319
+ wecrmAccounts: [],
3320
+ isLoadingSenderDetails: false,
3321
+ orgUnitId: -1,
2600
3322
  };
2601
3323
 
2602
3324
  // ============================================