@capillarytech/creatives-library 9.0.14 → 9.0.15-alpha.1

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 (175) hide show
  1. package/constants/unified.js +32 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/common.js +8 -0
  6. package/utils/commonUtils.js +19 -1
  7. package/utils/rcsPayloadUtils.js +92 -0
  8. package/utils/templateVarUtils.js +201 -0
  9. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  10. package/utils/tests/templateVarUtils.test.js +204 -0
  11. package/v2Components/CapActionButton/constants.js +7 -0
  12. package/v2Components/CapActionButton/index.js +168 -109
  13. package/v2Components/CapActionButton/index.scss +156 -5
  14. package/v2Components/CapActionButton/messages.js +19 -3
  15. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  16. package/v2Components/CapImageUpload/index.js +2 -2
  17. package/v2Components/CapTagList/index.js +10 -0
  18. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  24. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +60 -11
  25. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +201 -23
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +355 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +761 -222
  32. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  35. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  36. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +284 -267
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  39. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  40. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  41. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  43. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  44. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  45. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  46. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/FormBuilder/Classic.js +4487 -0
  49. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +366 -0
  50. package/v2Components/FormBuilder/Functional/channels/registry.js +17 -0
  51. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +9 -0
  52. package/v2Components/FormBuilder/Functional/channels/sms/config.js +30 -0
  53. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +46 -0
  54. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +13 -0
  55. package/v2Components/FormBuilder/Functional/channels/sms/index.js +22 -0
  56. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +52 -0
  57. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +25 -0
  58. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +87 -0
  59. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +89 -0
  60. package/v2Components/FormBuilder/Functional/constants.js +42 -0
  61. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +38 -0
  62. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +85 -0
  63. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +81 -0
  64. package/v2Components/FormBuilder/Functional/core/store/selectors.js +30 -0
  65. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +91 -0
  66. package/v2Components/FormBuilder/Functional/index.js +26 -0
  67. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +59 -0
  68. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +31 -0
  69. package/v2Components/FormBuilder/Functional/layout/Section.js +116 -0
  70. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +258 -0
  71. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +21 -0
  72. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +65 -0
  73. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +97 -0
  74. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +192 -0
  75. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +129 -0
  76. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +132 -0
  77. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +40 -0
  78. package/v2Components/FormBuilder/Functional/tests/section.test.js +99 -0
  79. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +67 -0
  80. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +155 -0
  81. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +172 -0
  82. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +122 -0
  83. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +329 -0
  84. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +160 -0
  85. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +95 -0
  86. package/v2Components/FormBuilder/_formBuilder.scss +5 -0
  87. package/v2Components/FormBuilder/index.js +41 -4479
  88. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +114 -0
  89. package/v2Components/FormBuilder/tests/entryGate.test.js +106 -0
  90. package/v2Components/FormBuilder/tests/sms.characterization.test.js +336 -0
  91. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  92. package/v2Components/SmsFallback/constants.js +94 -0
  93. package/v2Components/SmsFallback/index.js +958 -0
  94. package/v2Components/SmsFallback/index.scss +266 -0
  95. package/v2Components/SmsFallback/messages.js +78 -0
  96. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  97. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  98. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  99. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  100. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  101. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  102. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  103. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  104. package/v2Components/TemplatePreview/_templatePreview.scss +41 -22
  105. package/v2Components/TemplatePreview/constants.js +2 -0
  106. package/v2Components/TemplatePreview/index.js +143 -31
  107. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  108. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  109. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  110. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  111. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  112. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  113. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  114. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  115. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  116. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  117. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  118. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  119. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  120. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  121. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  122. package/v2Containers/CreativesContainer/constants.js +11 -0
  123. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  124. package/v2Containers/CreativesContainer/index.js +323 -104
  125. package/v2Containers/CreativesContainer/index.scss +83 -1
  126. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  127. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  128. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  129. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  130. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +333 -90
  131. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  132. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  133. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  134. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  135. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  136. package/v2Containers/Rcs/constants.js +131 -11
  137. package/v2Containers/Rcs/index.js +2602 -807
  138. package/v2Containers/Rcs/index.scss +289 -8
  139. package/v2Containers/Rcs/messages.js +34 -3
  140. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +227 -0
  141. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +73535 -38537
  142. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -8
  143. package/v2Containers/Rcs/tests/index.test.js +147 -36
  144. package/v2Containers/Rcs/tests/mockData.js +38 -0
  145. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  146. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  147. package/v2Containers/Rcs/utils.js +478 -11
  148. package/v2Containers/Sms/Create/index.js +115 -48
  149. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  150. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  151. package/v2Containers/SmsTrai/Create/index.js +9 -4
  152. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  153. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  154. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  155. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  156. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  157. package/v2Containers/SmsWrapper/index.js +37 -8
  158. package/v2Containers/TagList/index.js +6 -0
  159. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  160. package/v2Containers/Templates/_templates.scss +171 -12
  161. package/v2Containers/Templates/actions.js +11 -0
  162. package/v2Containers/Templates/constants.js +2 -0
  163. package/v2Containers/Templates/index.js +125 -55
  164. package/v2Containers/Templates/sagas.js +57 -13
  165. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  166. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  167. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  168. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  169. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  170. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  171. package/v2Containers/TemplatesV2/index.js +86 -23
  172. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  173. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  174. package/v2Containers/Whatsapp/index.js +3 -20
  175. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -1,13 +1,12 @@
1
1
  /* eslint-disable no-unused-expressions */
2
- import React, { useState, useEffect, useCallback } from 'react';
2
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
3
3
  import { bindActionCreators } from 'redux';
4
4
  import { createStructuredSelector } from 'reselect';
5
- import { injectIntl, FormattedMessage } from 'react-intl';
5
+ import { FormattedMessage } from 'react-intl';
6
6
  import get from 'lodash/get';
7
7
  import isEmpty from 'lodash/isEmpty';
8
8
  import cloneDeep from 'lodash/cloneDeep';
9
9
  import isNil from 'lodash/isNil';
10
- import styled from 'styled-components';
11
10
  import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
12
11
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
13
12
  import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
@@ -35,6 +34,14 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
35
34
  import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
36
35
  import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
37
36
  import CapLink from '@capillarytech/cap-ui-library/CapLink';
37
+ import CapTab from '@capillarytech/cap-ui-library/CapTab';
38
+ import { flushSync } from 'react-dom';
39
+ import { isUrl, isValidText } from '../Line/Container/Wrapper/utils';
40
+ import {
41
+ invalidVarRegex,
42
+ RCS_CTA_URL_TYPE,
43
+ URL_MAX_LENGTH,
44
+ } from '../../v2Components/CapActionButton/constants';
38
45
 
39
46
  import {
40
47
  CAP_G01,
@@ -50,17 +57,30 @@ import {
50
57
  import CapVideoUpload from '../../v2Components/CapVideoUpload';
51
58
  import * as globalActions from '../Cap/actions';
52
59
  import CapActionButton from '../../v2Components/CapActionButton';
60
+ import TemplatePreview from '../../v2Components/TemplatePreview';
53
61
  import { makeSelectRcs, makeSelectAccount } from './selectors';
54
62
  import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
55
63
  import {
56
64
  isLoadingMetaEntities,
57
65
  makeSelectMetaEntities,
58
66
  setInjectedTags,
67
+ selectCurrentOrgDetails,
59
68
  } from '../Cap/selectors';
60
69
  import * as RcsActions from './actions';
61
70
  import { isAiContentBotDisabled } from '../../utils/common';
62
71
  import * as TemplatesActions from '../Templates/actions';
63
72
  import './index.scss';
73
+ import {
74
+ normalizeLibraryLoadedTitleDesc,
75
+ mergeRcsSmsFallBackContentFromDetails,
76
+ mergeRcsSmsFallbackVarMapLayers,
77
+ extractRegisteredSenderIdsFromSmsFallbackRecord,
78
+ pickFirstSmsFallbackTemplateString,
79
+ syncCardVarMappedSemanticsFromSlots,
80
+ hasMeaningfulSmsFallbackShape,
81
+ getLibrarySmsFallbackApiBaselineFromTemplateData,
82
+ pickRcsCardVarMappedEntries,
83
+ } from './rcsLibraryHydrationUtils';
64
84
  import {
65
85
  RCS,
66
86
  SMS,
@@ -77,6 +97,7 @@ import {
77
97
  RCS_IMG_SIZE,
78
98
  RCS_DLT_MODE,
79
99
  CTA,
100
+ AI_CONTENT_BOT_DISABLED,
80
101
  RCS_STATUSES,
81
102
  TITLE_TEXT,
82
103
  MESSAGE_TEXT,
@@ -91,9 +112,15 @@ import {
91
112
  rcsVarTestRegex,
92
113
  RCS_IMAGE_DIMENSIONS,
93
114
  RCS_TEXT_MESSAGE_MAX_LENGTH,
115
+ RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
94
116
  RCS_RICH_CARD_MAX_LENGTH,
95
117
  RCS_VIDEO_THUMBNAIL_DIMENSIONS,
118
+ RCS_CAROUSEL_IMAGE_DIMENSIONS,
119
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
120
+ RCS_CAROUSEL_IMG_SIZE,
121
+ RCS_CAROUSEL_VIDEO_SIZE,
96
122
  MAX_BUTTONS,
123
+ INITIAL_SUGGESTIONS,
97
124
  INITIAL_SUGGESTIONS_DATA_STOP,
98
125
  RCS_BUTTON_TYPES,
99
126
  titletype,
@@ -103,25 +130,50 @@ import {
103
130
  SMALL,
104
131
  MEDIUM,
105
132
  RICHCARD,
133
+ HOST_INFOBIP,
134
+ HOST_ICS,
135
+ CAROUSEL_HEIGHT_OPTIONS,
136
+ CAROUSEL_WIDTH_OPTIONS,
137
+ STOP,
138
+ RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
139
+ RCS_NUMERIC_VAR_NAME_REGEX,
140
+ RCS_NUMERIC_VAR_TOKEN_REGEX,
141
+ RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
142
+ RCS_TAG_AREA_FIELD_TITLE,
143
+ RCS_TAG_AREA_FIELD_DESC,
144
+ REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
145
+ JS_TYPES,
106
146
  } from './constants';
107
147
  import globalMessages from '../Cap/messages';
108
148
  import messages from './messages';
109
149
  import creativesMessages from '../CreativesContainer/messages';
110
150
  import withCreatives from '../../hoc/withCreatives';
111
151
  import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
112
- import { ANDROID } from '../../v2Components/CommonTestAndPreview/constants';
152
+ import VarSegmentMessageEditor from '../../v2Components/VarSegmentMessageEditor';
153
+ import { ANDROID, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
113
154
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
155
+ import { splitTemplateVarString } from '../../utils/templateVarUtils';
114
156
  import CapImageUpload from '../../v2Components/CapImageUpload';
115
- import addCreativesIcon from '../Assets/images/addCreativesIllustration.svg';
116
157
  import Templates from '../Templates';
117
158
  import SmsTraiEdit from '../SmsTrai/Edit';
159
+ import SmsFallback from '../../v2Components/SmsFallback';
160
+ import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
118
161
  import TagList from '../TagList';
119
162
  import { validateTags } from '../../utils/tagValidations';
120
- import { getCdnUrl } from '../../utils/cdnTransformation';
163
+ import { isTraiDLTEnable } from '../../utils/common';
121
164
  import { isTagIncluded } from '../../utils/commonUtils';
122
165
  import injectReducer from '../../utils/injectReducer';
123
166
  import v2RcsReducer from './reducer';
124
- import { getTemplateStatusType } from './utils';
167
+ import {
168
+ areAllRcsSmsFallbackVarSlotsFilled,
169
+ buildRcsNumericMustachePlaceholderRegex,
170
+ getTemplateStatusType,
171
+ normalizeCardVarMapped,
172
+ coalesceCardVarMappedToTemplate,
173
+ getRcsSemanticVarNamesSpanningTitleAndDesc,
174
+ resolveCardVarMappedSlotValue,
175
+ sanitizeCardVarMappedValue,
176
+ } from './utils';
125
177
 
126
178
 
127
179
  const { Group: CapCheckboxGroup } = CapCheckbox;
@@ -138,19 +190,18 @@ export const Rcs = (props) => {
138
190
  templatesActions,
139
191
  globalActions,
140
192
  location,
141
- handleClose,
142
193
  getDefaultTags,
143
194
  supportedTags,
144
195
  metaEntities,
145
196
  injectedTags,
146
197
  loadingTags,
147
198
  getFormData,
148
- isDltEnabled,
149
199
  smsRegister,
200
+ orgUnitId,
150
201
  selectedOfferDetails,
151
202
  eventContextTags,
152
- waitEventContextTags,
153
203
  accountData = {},
204
+ currentOrgDetails,
154
205
  // TestAndPreviewSlidebox props
155
206
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
156
207
  handleTestAndPreview: propsHandleTestAndPreview,
@@ -159,7 +210,25 @@ export const Rcs = (props) => {
159
210
  const { formatMessage } = intl;
160
211
  const { TextArea } = CapInput;
161
212
  const { CapCustomCardList } = CapCustomCard;
213
+
214
+ // Defensive: React cannot render plain objects as children (crashes with
215
+ // "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
216
+ // can accidentally set an error state to an object (e.g. `{}`).
217
+ const normalizeErrorMessage = (err) => {
218
+ if (!err) return '';
219
+ if (React.isValidElement(err)) return err;
220
+ if (typeof err === JS_TYPES.STRING) return err;
221
+ if (typeof err === JS_TYPES.NUMBER) return String(err);
222
+ if (typeof err === JS_TYPES.OBJECT && typeof err.message === JS_TYPES.STRING) return err.message;
223
+ try {
224
+ return JSON.stringify(err);
225
+ } catch (e) {
226
+ return String(err);
227
+ }
228
+ };
229
+
162
230
  const [isEditFlow, setEditFlow] = useState(false);
231
+ const isEditLike = isEditFlow || !isFullMode;
163
232
  const [tags, updateTags] = useState([]);
164
233
  const [spin, setSpin] = useState(false);
165
234
  //template
@@ -168,33 +237,21 @@ export const Rcs = (props) => {
168
237
  const [templateMediaType, setTemplateMediaType] = useState(
169
238
  RCS_MEDIA_TYPES.NONE,
170
239
  );
171
- const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
172
240
  const [templateTitle, setTemplateTitle] = useState('');
173
241
  const [templateDesc, setTemplateDesc] = useState('');
174
242
  const [templateDescError, setTemplateDescError] = useState(false);
175
243
  const [templateStatus, setTemplateStatus] = useState('');
176
- const [templateDate, setTemplateDate] = useState('');
177
- //fallback
178
- const [fallbackMessage, setFallbackMessage] = useState('');
179
- const [fallbackMessageError, setFallbackMessageError] = useState(false);
180
244
  //fallback dlt
181
245
  const [showDltContainer, setShowDltContainer] = useState(false);
182
246
  const [dltMode, setDltMode] = useState('');
183
247
  const [dltEditData, setDltEditData] = useState({});
184
- const [showDltCard, setShowDltCard] = useState(false);
185
- const [fallbackPreviewmode, setFallbackPreviewmode] = useState(false);
186
- const [dltPreviewData, setDltPreviewData] = useState('');
248
+ /** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
249
+ const [smsFallbackData, setSmsFallbackData] = useState(undefined);
187
250
  const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
188
- const [buttonType, setButtonType] = useState(RCS_BUTTON_TYPES.NONE);
251
+ const buttonType = RCS_BUTTON_TYPES.NONE;
189
252
  const [suggestionError, setSuggestionError] = useState(true);
253
+ const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
190
254
  const [templateType, setTemplateType] = useState('text_message');
191
- const [templateHeader, setTemplateHeader] = useState('');
192
- const [templateMessage, setTemplateMessage] = useState('');
193
- const [templateHeaderError, setTemplateHeaderError] = useState('');
194
- const [templateMessageError, setTemplateMessageError] = useState('');
195
- const validVarRegex = /\{\{(\d+)\}\}/g;
196
- const [updatedTitleData, setUpdatedTitleData] = useState([]);
197
- const [updatedDescData, setUpdatedDescData] = useState([]);
198
255
  const [titleVarMappedData, setTitleVarMappedData] = useState({});
199
256
  const [descVarMappedData, setDescVarMappedData] = useState({});
200
257
  const [titleTextAreaId, setTitleTextAreaId] = useState();
@@ -205,75 +262,58 @@ export const Rcs = (props) => {
205
262
  const [rcsVideoSrc, setRcsVideoSrc] = useState({});
206
263
  const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
207
264
  const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
208
- const [imageError, setImageError] = useState(null);
265
+ // Carousel (UI-only) state
266
+ const [selectedCarousel, setSelectedCarousel] = useState('');
267
+ const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
268
+ const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
269
+ const [carouselData, setCarouselData] = useState([]);
270
+ const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
271
+ const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
272
+ const [carouselResetNonce, setCarouselResetNonce] = useState(0);
273
+ const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
274
+ const [imageError, setImageError] = useState(null);
209
275
  const [templateTitleError, setTemplateTitleError] = useState(false);
210
276
  const [cardVarMapped, setCardVarMapped] = useState({});
277
+ /** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
278
+ const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
279
+ const lastHydratedRcsCardVarSignatureRef = useRef(null);
280
+
281
+ /**
282
+ * Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
283
+ * without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
284
+ * fallback card / content stopped appearing until re-selected.
285
+ */
286
+ const rcsHydrationDetails = useMemo(
287
+ () => (isFullMode ? rcsData?.templateDetails : templateData),
288
+ [isFullMode, rcsData?.templateDetails, templateData],
289
+ );
211
290
 
212
- // TestAndPreviewSlidebox state
213
- const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
214
- const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
215
-
216
- const tempMsg = dltPreviewData === '' ? fallbackMessage : dltPreviewData;
217
-
218
- // Get template content for TestAndPreviewSlidebox
219
- // Reference: Based on getRcsPreview() function (lines 2087-2111) which prepares content for TemplatePreview
220
- // getRcsPreview ALWAYS uses templateTitle and templateDesc for ALL template types (text_message, rich_card, carousel)
221
- // renderTextComponent (lines 1317-1485) also uses templateTitle and templateDesc
222
- // Note: templateHeader and templateMessage are defined but NOT used in the component
223
- const getTemplateContent = useCallback(() => {
224
- const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
225
- const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
226
- const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
227
-
228
- // Build media preview object (same pattern as getRcsPreview)
229
- const mediaPreview = {};
230
- if (isMediaTypeImage && rcsImageSrc) {
231
- mediaPreview.rcsImageSrc = rcsImageSrc;
232
- }
233
- if (isMediaTypeVideo && !isMediaTypeText) {
234
- // For video, use thumbnailSrc as rcsVideoSrc (same as getRcsPreview line 2104)
235
- if (rcsThumbnailSrc) {
236
- mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
237
- } else if (rcsVideoSrc?.videoSrc) {
238
- mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
239
- }
240
- // Also include thumbnailSrc separately if available
241
- if (rcsThumbnailSrc) {
242
- mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
291
+ /** Skip duplicate /meta/TAG fetches: same query is triggered from (1) useEffect below, (2) title TagList mount, (3) description TagList mount — each calls getTagsforContext('Outbound'). */
292
+ const lastTagSchemaQueryKeyRef = useRef(null);
293
+ /**
294
+ * Library: parent often passes a new `templateData` object reference every render. Re-applying the same
295
+ * SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
296
+ */
297
+ const lastSmsFallbackHydrationKeyRef = useRef(null);
298
+
299
+ const fetchTagSchemaIfNewQuery = useCallback(
300
+ (query) => {
301
+ const key = JSON.stringify(query);
302
+ if (lastTagSchemaQueryKeyRef.current === key) {
303
+ return;
243
304
  }
244
- }
245
-
246
- // Build content object
247
- // Reference: getRcsPreview (line 2091-2092) uses templateTitle and templateDesc for ALL cases
248
- // templateTitle is used for rich_card/carousel title, empty for text_message
249
- // templateDesc is used for ALL types (text message body or rich card description)
250
- // For UnifiedPreview, we map templateTitle -> templateHeader and templateDesc -> templateMessage
251
- const contentObj = {
252
- // Map templateTitle to templateHeader and templateDesc to templateMessage
253
- templateHeader: templateTitle,
254
- templateMessage: templateDesc,
255
- ...mediaPreview,
256
- ...(suggestions.length > 0 && {
257
- suggestions: suggestions,
258
- }),
259
- };
305
+ lastTagSchemaQueryKeyRef.current = key;
306
+ globalActions.fetchSchemaForEntity(query);
307
+ },
308
+ [globalActions],
309
+ );
260
310
 
261
- return contentObj;
262
- }, [
263
- templateMediaType,
264
- templateTitle,
265
- templateDesc,
266
- rcsImageSrc,
267
- rcsVideoSrc,
268
- rcsThumbnailSrc,
269
- suggestions,
270
- selectedDimension,
271
- ]);
311
+ // TestAndPreviewSlidebox state
312
+ const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
272
313
 
273
314
  // Handle Test and Preview button click
274
315
  const handleTestAndPreview = useCallback(() => {
275
316
  setShowTestAndPreviewSlidebox(true);
276
- setIsTestAndPreviewMode(true);
277
317
  if (propsHandleTestAndPreview) {
278
318
  propsHandleTestAndPreview();
279
319
  }
@@ -297,31 +337,723 @@ export const Rcs = (props) => {
297
337
  // For video
298
338
  return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
299
339
  };
340
+ /** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
341
+ const handleSmsFallbackEditorStateChange = useCallback((patch) => {
342
+ setSmsFallbackData((prev) => {
343
+ if (!patch || typeof patch !== 'object') return prev;
344
+ // Bail out when no template has been selected yet — SmsTraiEdit fires
345
+ // onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
346
+ // which would create a non-null smsFallbackData from nothing and cause
347
+ // the card to appear as "Untitled creative" before any save.
348
+ if (!prev) return prev;
349
+ return { ...prev, ...patch };
350
+ });
351
+ }, []);
300
352
 
353
+ /** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
301
354
  const [accountId, setAccountId] = useState('');
355
+ /** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
356
+ const [wecrmAccountId, setWecrmAccountId] = useState('');
302
357
  const [accessToken, setAccessToken] = useState('');
303
358
  const [hostName, setHostName] = useState('');
304
359
  const [accountName, setAccountName] = useState('');
360
+ const isHostInfoBip = hostName === HOST_INFOBIP;
361
+ const isHostIcs = hostName === HOST_ICS;
362
+
363
+ useEffect(() => {
364
+ setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
365
+ }, [isHostIcs]);
305
366
  const [rcsAccount, setRcsAccount] = useState('');
306
367
  useEffect(() => {
307
368
  const accountObj = accountData.selectedRcsAccount || {};
308
369
  if (!isEmpty(accountObj)) {
309
370
  const {
371
+ id: wecrmId,
310
372
  sourceAccountIdentifier = '',
311
373
  configs = {},
312
374
  } = accountObj;
313
-
314
375
  setAccountId(sourceAccountIdentifier);
376
+ setWecrmAccountId(
377
+ wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
378
+ );
315
379
  setAccessToken(configs.accessToken || '');
316
380
  setHostName(accountObj.hostName || '');
317
381
  setAccountName(accountObj.name || '');
318
382
  setRcsAccount(accountObj.id || '');
383
+ } else {
384
+ setAccountId('');
385
+ setWecrmAccountId('');
386
+ setAccessToken('');
387
+ setHostName('');
388
+ setAccountName('');
319
389
  }
320
390
  }, [accountData.selectedRcsAccount]);
321
391
 
322
392
  const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
323
393
  const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
324
394
  const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
395
+ const isCarouselType = templateType === contentType.carousel;
396
+
397
+ const MAX_RCS_CAROUSEL_ALLOWED = 10;
398
+ // Uploads for RCS are stored in redux under dynamic keys `uploadedAssetData${index}`.
399
+ // Carousel needs per-card indices; otherwise all cards "restore" the last uploaded asset
400
+ // and show the same media/thumbnail.
401
+ const RCS_CAROUSEL_ASSET_INDEX_BASE = 10; // keep away from standalone indices 0 (media) and 1 (thumbnail)
402
+ const getCarouselImageAssetIndex = (cardIndex) =>
403
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
404
+ const getCarouselVideoAssetIndex = (cardIndex) =>
405
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 1;
406
+ const getCarouselThumbnailAssetIndex = (cardIndex) =>
407
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 2;
408
+ const isThumbnailAssetIndex = (index) => {
409
+ if (index === 1) return true; // standalone thumbnail
410
+ if (index >= RCS_CAROUSEL_ASSET_INDEX_BASE) {
411
+ return ((index - RCS_CAROUSEL_ASSET_INDEX_BASE) % 3) === 2; // carousel thumbnail slot
412
+ }
413
+ return false;
414
+ };
415
+
416
+ // Carousel dimension key: `${HEIGHT}_${WIDTH}` (e.g., SHORT_SMALL)
417
+ const getCarouselDimensionKey = () => `${selectedCarouselHeight}_${selectedCarouselWidth}`;
418
+
419
+ const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
420
+ setCarouselData((prev = []) => {
421
+ const updated = cloneDeep(prev);
422
+ if (!updated?.[cardIndex]) return updated;
423
+ if (clearImage) updated[cardIndex].imageSrc = '';
424
+ if (clearVideo) updated[cardIndex].videoAsset = {};
425
+ if (clearThumb) updated[cardIndex].thumbnailSrc = '';
426
+ return updated;
427
+ });
428
+
429
+ if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
430
+ if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
431
+ if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
432
+ };
433
+
434
+ const resetCarouselMediaForAllCards = () => {
435
+ setCarouselData((prev = []) => {
436
+ const updated = cloneDeep(prev);
437
+ updated.forEach((card) => {
438
+ if (!card) return;
439
+ card.imageSrc = '';
440
+ card.videoAsset = {};
441
+ card.thumbnailSrc = '';
442
+ });
443
+ return updated;
444
+ });
445
+ (carouselData || []).forEach((_, idx) => {
446
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
447
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
448
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
449
+ });
450
+ // Force tab panes to remount after global reset (some tab implementations cache inactive panes)
451
+ setCarouselResetNonce((n) => n + 1);
452
+ };
453
+
454
+ const RCS_CAROUSEL_INITIAL_CARD = {
455
+ title: '',
456
+ description: '',
457
+ mediaType: RCS_MEDIA_TYPES.IMAGE, // per-card
458
+ imageSrc: '',
459
+ videoAsset: {}, // CapVideoUpload object shape
460
+ thumbnailSrc: '',
461
+ suggestions: [],
462
+ };
463
+
464
+ const RCS_CAROUSEL_INITIAL_FIRST_CARD = {
465
+ ...RCS_CAROUSEL_INITIAL_CARD,
466
+ suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
467
+ };
468
+
469
+ const ensureFirstCardDefaultPhoneSuggestions = (cards) => {
470
+ const next = cloneDeep(cards || []);
471
+ if (next.length === 0) return next;
472
+ const s = next[0].suggestions;
473
+ if (!Array.isArray(s) || s.length === 0) {
474
+ next[0] = {
475
+ ...next[0],
476
+ suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
477
+ };
478
+ }
479
+ return next;
480
+ };
481
+
482
+ // Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
483
+ // updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
484
+ // suggestions typed since the last commit (stale closure).
485
+ const handleCarouselValueChange = (carouselIndex, fields) => {
486
+ setCarouselData((prev = []) => {
487
+ const updated = cloneDeep(prev);
488
+ if (!updated[carouselIndex]) return prev;
489
+ fields.forEach(({ fieldName, value }) => {
490
+ updated[carouselIndex][fieldName] = value;
491
+ });
492
+ return updated;
493
+ });
494
+ };
495
+
496
+ const updateCarouselErrors = (carouselIndex, patch) => {
497
+ setCarouselErrors((prev = []) => {
498
+ const next = Array.isArray(prev) ? [...prev] : [];
499
+ next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
500
+ return next;
501
+ });
502
+ };
503
+
504
+ const deleteCarouselCard = (index) => {
505
+ let nextIdx = 0;
506
+ flushSync(() => {
507
+ setCarouselData((prev = []) => {
508
+ const updated = cloneDeep(prev);
509
+ if (index < 0 || index >= updated.length) return updated;
510
+ updated.splice(index, 1);
511
+ nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
512
+ return ensureFirstCardDefaultPhoneSuggestions(updated);
513
+ });
514
+ });
515
+ setCarouselErrors((prev = []) => {
516
+ const next = Array.isArray(prev) ? [...prev] : [];
517
+ next.splice(index, 1);
518
+ return next;
519
+ });
520
+ setActiveCarouselIndex(`${nextIdx}`);
521
+ };
522
+
523
+ const carouselButtonTextHasForbiddenChars = (value) => {
524
+ if (!value) return false;
525
+ if (value.includes('[') || value.includes(']')) return true;
526
+ const withoutValidVariables = value.replace(/\{\{[^}]*\}\}/g, '');
527
+ if (withoutValidVariables.includes('{') || withoutValidVariables.includes('}')) return true;
528
+ return false;
529
+ };
530
+
531
+ const isCompleteSavedCarouselSuggestion = (s) => {
532
+ if (!s || !s.isSaved) return false;
533
+ const text = (s.text || '').trim();
534
+ if (!text || !isValidText(text) || carouselButtonTextHasForbiddenChars(text)) return false;
535
+ if (s.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
536
+ return String(s.phoneNumber || '').length >= 5;
537
+ }
538
+ if (s.type === RCS_BUTTON_TYPES.CTA) {
539
+ const url = String(s.url || '').trim();
540
+ if (!url || url.length > URL_MAX_LENGTH) return false;
541
+ const subtype = s.urlType || RCS_CTA_URL_TYPE.STATIC;
542
+ if (subtype === RCS_CTA_URL_TYPE.DYNAMIC) {
543
+ return true;
544
+ }
545
+ if (!isUrl(url)) return false;
546
+ const varMatches = url.match(invalidVarRegex);
547
+ return !(varMatches && varMatches.length > 0);
548
+ }
549
+ if (s.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
550
+ return true;
551
+ }
552
+ return false;
553
+ };
554
+
555
+ const isCarouselCardValid = (card, cardIndex) => {
556
+ if (!card) return false;
557
+ if (!card.title || !card.title.trim()) return false;
558
+ if ((card.title || '').length > TEMPLATE_TITLE_MAX_LENGTH) return false;
559
+ if (!card.description || !card.description.trim()) return false;
560
+ if ((card.description || '').length > RCS_RICH_CARD_MAX_LENGTH) return false;
561
+ let mediaOk = false;
562
+ if (card.mediaType === RCS_MEDIA_TYPES.IMAGE) {
563
+ mediaOk = !!(card.imageSrc && String(card.imageSrc).trim());
564
+ } else if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
565
+ const hasVideo = !!(card.videoAsset && card.videoAsset.videoSrc && String(card.videoAsset.videoSrc).trim());
566
+ const hasThumb = !!(card.thumbnailSrc && String(card.thumbnailSrc).trim());
567
+ mediaOk = hasVideo && hasThumb;
568
+ } else {
569
+ return false;
570
+ }
571
+ if (!mediaOk) return false;
572
+ if (cardIndex === 0) {
573
+ const sugg = Array.isArray(card.suggestions) ? card.suggestions : [];
574
+ if (!sugg.some(isCompleteSavedCarouselSuggestion)) return false;
575
+ }
576
+ return true;
577
+ };
578
+
579
+ const checkDisableAddCarouselButton = () => {
580
+ const idx = parseInt(activeCarouselIndex, 10);
581
+ const activeCard = carouselData?.[idx];
582
+ return !isCarouselCardValid(activeCard, idx);
583
+ };
584
+
585
+ const addCarouselCard = () => {
586
+ let newIndex = 0;
587
+ flushSync(() => {
588
+ setCarouselData((prev = []) => {
589
+ const updated = cloneDeep(prev);
590
+ updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
591
+ newIndex = updated.length - 1;
592
+ return updated;
593
+ });
594
+ });
595
+ setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
596
+ setActiveCarouselIndex(`${newIndex}`);
597
+ };
598
+
599
+ // Initialize carousel data when switching to carousel type
600
+ useEffect(() => {
601
+ if (!isCarouselType) return;
602
+ if (!carouselData || carouselData.length === 0) {
603
+ setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
604
+ setCarouselErrors([{}]);
605
+ setActiveCarouselIndex('0');
606
+ }
607
+ }, [isCarouselType]);
608
+
609
+ // keep derived carousel key in sync
610
+ useEffect(() => {
611
+ if (!isCarouselType) return;
612
+ if (!selectedCarouselHeight || !selectedCarouselWidth) return;
613
+ // Required format: HEIGHT_WIDTH
614
+ setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
615
+ }, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
616
+
617
+ const renderCarouselDimensionSelection = () => {
618
+ if (!isCarouselType) return null;
619
+ return (
620
+ <CapRow className="rcs-carousel-dimension-section">
621
+ <CapRow gutter={16} className="rcs-carousel-dimension-row">
622
+ <CapColumn span={12}>
623
+ <CapHeading type="h4" className="rcs-carousel-dimension-label">Card height</CapHeading>
624
+ <CapSelect
625
+ id="rcs-carousel-height-select"
626
+ value={selectedCarouselHeight}
627
+ onChange={(val) => {
628
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
629
+ resetCarouselMediaForAllCards();
630
+ setSelectedCarouselHeight(val);
631
+ }}
632
+ options={CAROUSEL_HEIGHT_OPTIONS}
633
+ disabled={isEditFlow || !isFullMode}
634
+ />
635
+ </CapColumn>
636
+ <CapColumn span={12}>
637
+ <CapHeading type="h4" className="rcs-carousel-dimension-label">Card width</CapHeading>
638
+ <CapSelect
639
+ id="rcs-carousel-width-select"
640
+ value={selectedCarouselWidth}
641
+ onChange={(val) => {
642
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
643
+ resetCarouselMediaForAllCards();
644
+ setSelectedCarouselWidth(val);
645
+ }}
646
+ options={CAROUSEL_WIDTH_OPTIONS}
647
+ disabled={isEditFlow || !isFullMode}
648
+ />
649
+ </CapColumn>
650
+ </CapRow>
651
+ {!!selectedCarousel && (
652
+ <CapLabel type="label3" className="rcs-carousel-selected-dimension">
653
+ Selected: {selectedCarousel}
654
+ </CapLabel>
655
+ )}
656
+ </CapRow>
657
+ );
658
+ };
659
+
660
+ // Reuse rich-card buttons UI per carousel card
661
+ const renderButtonComponentForCarouselCard = (cardIndex) => {
662
+ const card = carouselData?.[cardIndex] || {};
663
+ const suggestionsForCard = card.suggestions || [];
664
+ return (
665
+ <>
666
+ <CapHeader
667
+ className="rcs-button-cta"
668
+ title={(
669
+ <CapRow type="flex">
670
+ <CapHeading type="h4">
671
+ {formatMessage(messages.btnLabel)}
672
+ </CapHeading>
673
+ </CapRow>
674
+ )}
675
+ description={(
676
+ <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
677
+ )}
678
+ />
679
+ <CapActionButton
680
+ buttonType={RCS_BUTTON_TYPES.NONE}
681
+ updateButtonChange={(data, btnIndex) => {
682
+ // Match existing behavior: allow CapActionButton to manage save gating.
683
+ const updated = cloneDeep(suggestionsForCard);
684
+ if (btnIndex === MAX_BUTTONS) {
685
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: data }]);
686
+ return;
687
+ }
688
+ updated[btnIndex] = data;
689
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
690
+ }}
691
+ deleteButtonHandler={(btnIndex) => {
692
+ if (cardIndex === 0 && btnIndex === 0) {
693
+ return;
694
+ }
695
+ const savedCount = (suggestionsForCard || []).filter((x) => x && x.isSaved).length;
696
+ const target = (suggestionsForCard || []).find((s) => s && s.index === btnIndex);
697
+ if (cardIndex === 0 && target?.isSaved && savedCount <= 1) {
698
+ return;
699
+ }
700
+ const updated = cloneDeep(suggestionsForCard)
701
+ .filter((i) => i.index !== btnIndex)
702
+ .map((i, idx) => ({ ...i, index: idx }));
703
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
704
+ }}
705
+ suggestions={suggestionsForCard}
706
+ isEditFlow={isEditFlow}
707
+ isFullMode={isFullMode}
708
+ maxButtons={MAX_BUTTONS}
709
+ host={hostName}
710
+ minSavedSuggestions={cardIndex === 0 ? 1 : 0}
711
+ hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
712
+ />
713
+ </>
714
+ );
715
+ };
716
+
717
+ const renderCarouselCardMedia = (cardIndex) => {
718
+ const card = carouselData?.[cardIndex] || {};
719
+ const dimKey = getCarouselDimensionKey();
720
+
721
+ if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
722
+ return (
723
+ <>
724
+ <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Video</CapHeading>
725
+ <CapVideoUpload
726
+ index={getCarouselVideoAssetIndex(cardIndex)}
727
+ allowedExtensionsRegex={ALLOWED_EXTENSIONS_VIDEO_REGEX}
728
+ videoSize={RCS_CAROUSEL_VIDEO_SIZE}
729
+ isFullMode={isFullMode}
730
+ uploadAsset={uploadRcsVideo}
731
+ uploadedAssetList={card.videoAsset || {}}
732
+ onVideoUploadUpdateAssestList={(_, val) => {
733
+ handleCarouselValueChange(cardIndex, [{ fieldName: 'videoAsset', value: val }]);
734
+ }}
735
+ videoData={rcsData}
736
+ className="cap-custom-video-upload"
737
+ formClassName={"rcs-video-upload"}
738
+ channel={RCS}
739
+ errorMessage={formatMessage(messages.videoErrorMessage)}
740
+ showVideoNameAndDuration={false}
741
+ showReUploadButton={!isEditFlow && isFullMode}
742
+ channelSpecificStyle={!isFullMode}
743
+ />
744
+
745
+ <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
746
+ <CapImageUpload
747
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
748
+ imgWidth={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.width}
749
+ imgHeight={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.height}
750
+ imgSize={RCS_THUMBNAIL_MAX_SIZE}
751
+ uploadAsset={uploadRcsImage}
752
+ isFullMode={isFullMode}
753
+ imageSrc={card.thumbnailSrc}
754
+ updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: val }])}
755
+ updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: '' }])}
756
+ minImgSize={RCS_THUMBNAIL_MIN_SIZE}
757
+ index={getCarouselThumbnailAssetIndex(cardIndex)}
758
+ className="cap-custom-image-upload"
759
+ key={`rcs-carousel-thumb-${cardIndex}-${dimKey}`}
760
+ imageData={rcsData}
761
+ channel={RCS}
762
+ channelSpecificStyle={!isFullMode}
763
+ skipDimensionValidation={true}
764
+ showReUploadButton={!isEditFlow && isFullMode}
765
+ disabled={isEditFlow || !isFullMode}
766
+ />
767
+ </>
768
+ );
769
+ }
770
+
771
+ // Default: IMAGE
772
+ return (
773
+ <>
774
+ <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Image</CapHeading>
775
+ <CapImageUpload
776
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
777
+ imgWidth={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.width}
778
+ imgHeight={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.height}
779
+ imgSize={RCS_CAROUSEL_IMG_SIZE}
780
+ uploadAsset={uploadRcsImage}
781
+ isFullMode={isFullMode}
782
+ imageSrc={card.imageSrc}
783
+ updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: val }])}
784
+ updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: '' }])}
785
+ index={getCarouselImageAssetIndex(cardIndex)}
786
+ className="cap-custom-image-upload"
787
+ key={`rcs-carousel-image-${cardIndex}-${dimKey}`}
788
+ imageData={rcsData}
789
+ channel={RCS}
790
+ channelSpecificStyle={!isFullMode}
791
+ skipDimensionValidation={true}
792
+ showReUploadButton={!isEditFlow && isFullMode}
793
+ disabled={isEditFlow || !isFullMode}
794
+ />
795
+ </>
796
+ );
797
+ };
798
+
799
+ const renderCarouselCardButtons = (cardIndex) => {
800
+ return renderButtonComponentForCarouselCard(cardIndex);
801
+ };
802
+
803
+ const getCarouselTabPanes = () => {
804
+ return (carouselData || []).map((card, index) => {
805
+ return {
806
+ key: String(index),
807
+ tab: index + 1,
808
+ content: (
809
+ <CapCard
810
+ title={`Card ${index + 1}`}
811
+ extra={
812
+ !isEditFlow &&
813
+ (carouselData.length === 1 ? (
814
+ <CapTooltip title={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}>
815
+ <span className="button-disabled-tooltip-wrapper rcs-carousel-delete-tooltip-wrap">
816
+ <CapButton
817
+ className="rcs-carousel-card-delete"
818
+ type="flat"
819
+ onClick={() => deleteCarouselCard(index)}
820
+ disabled
821
+ aria-label={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}
822
+ >
823
+ <CapIcon type="delete" />
824
+ </CapButton>
825
+ </span>
826
+ </CapTooltip>
827
+ ) : (
828
+ <CapButton
829
+ className="rcs-carousel-card-delete"
830
+ type="flat"
831
+ onClick={() => deleteCarouselCard(index)}
832
+ aria-label={formatMessage(globalMessages.delete)}
833
+ >
834
+ <CapIcon type="delete" />
835
+ </CapButton>
836
+ ))
837
+ }
838
+ className="rcs-carousel-card"
839
+ >
840
+ {/* Media selection should be at top of card */}
841
+ <CapRow className="rcs-carousel-media-selection">
842
+ <CapColumn className="rcs-carousel-media-selection-heading">
843
+ <CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
844
+ </CapColumn>
845
+ <CapColumn>
846
+ <CapRadioGroup
847
+ id={`rcs-carousel-media-radio-${index}`}
848
+ options={mediaRadioOptions}
849
+ value={card.mediaType}
850
+ onChange={({ target: { value } }) => {
851
+ // Reset media fields when switching type
852
+ if (value === RCS_MEDIA_TYPES.IMAGE) {
853
+ // Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
854
+ clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
855
+ handleCarouselValueChange(index, [
856
+ { fieldName: 'mediaType', value },
857
+ { fieldName: 'videoAsset', value: {} },
858
+ { fieldName: 'thumbnailSrc', value: '' },
859
+ ]);
860
+ } else {
861
+ // Switching to VIDEO: clear image upload so it doesn't auto-restore.
862
+ clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
863
+ handleCarouselValueChange(index, [
864
+ { fieldName: 'mediaType', value },
865
+ { fieldName: 'imageSrc', value: '' },
866
+ ]);
867
+ }
868
+ }}
869
+ disabled={isEditFlow || !isFullMode}
870
+ className="rcs-radio"
871
+ />
872
+ </CapColumn>
873
+ </CapRow>
874
+
875
+ <CapRow className="rcs-carousel-media-upload">
876
+ {renderCarouselCardMedia(index)}
877
+ </CapRow>
878
+
879
+ {/* Title after media */}
880
+ <CapRow className="rcs-carousel-card-row">
881
+ <CapHeader
882
+ className="rcs-template-title-label"
883
+ title={<CapHeading type="h4">Card title</CapHeading>}
884
+ suffix={
885
+ (isEditFlow || !isFullMode) ? (
886
+ <TagList
887
+ label={formatMessage(globalMessages.addLabels)}
888
+ onTagSelect={onCarouselTagSelect}
889
+ location={location}
890
+ tags={getRcsTags()}
891
+ onContextChange={handleOnTagsContextChange}
892
+ injectedTags={injectedTags || {}}
893
+ selectedOfferDetails={selectedOfferDetails}
894
+ />
895
+ ) : (
896
+ <CapButton
897
+ data-testid={`rcs-carousel-title-add-var-${index}`}
898
+ type="flat"
899
+ isAddBtn
900
+ onClick={() => appendVarToCarouselField(index, 'title')}
901
+ disabled={!!carouselErrors?.[index]?.title}
902
+ >
903
+ {formatMessage(messages.addVar)}
904
+ </CapButton>
905
+ )
906
+ }
907
+ />
908
+ <CapRow className="rcs_text_area_wrapper">
909
+ {(isEditFlow || !isFullMode) ? (
910
+ renderCarouselEditMessage(card.title || '')
911
+ ) : (
912
+ <CapInput
913
+ value={card.title || ''}
914
+ placeholder={formatMessage(messages.templateTitlePlaceholder)}
915
+ onChange={({ target: { value } }) => {
916
+ let error = false;
917
+ if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
918
+ error = formatMessage(messages.templateHeaderLengthError);
919
+ } else {
920
+ error = variableErrorHandling(value);
921
+ }
922
+ updateCarouselErrors(index, { title: error });
923
+ handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
924
+ }}
925
+ disabled={isEditFlow || !isFullMode}
926
+ errorMessage={carouselErrors?.[index]?.title}
927
+ />
928
+ )}
929
+ </CapRow>
930
+ </CapRow>
931
+ {!isEditFlow && (
932
+ <CapRow className="rcs-carousel-character-count-row">
933
+ {renderCarouselCharacterCount(
934
+ getCarouselTitleCharacterCount(index),
935
+ getTitleMaxLength(),
936
+ )}
937
+ </CapRow>
938
+ )}
939
+
940
+ {/* Description after title */}
941
+ <CapRow className="rcs-carousel-card-row">
942
+ <CapHeader
943
+ title={<CapHeading type="h4">Card body text</CapHeading>}
944
+ suffix={
945
+ (isEditFlow || !isFullMode) ? (
946
+ <TagList
947
+ label={formatMessage(globalMessages.addLabels)}
948
+ onTagSelect={onCarouselTagSelect}
949
+ location={location}
950
+ tags={getRcsTags()}
951
+ onContextChange={handleOnTagsContextChange}
952
+ injectedTags={injectedTags || {}}
953
+ selectedOfferDetails={selectedOfferDetails}
954
+ />
955
+ ) : (
956
+ <CapButton
957
+ data-testid={`rcs-carousel-desc-add-var-${index}`}
958
+ type="flat"
959
+ isAddBtn
960
+ onClick={() => appendVarToCarouselField(index, 'description')}
961
+ disabled={!!carouselErrors?.[index]?.description}
962
+ >
963
+ {formatMessage(messages.addVar)}
964
+ </CapButton>
965
+ )
966
+ }
967
+ />
968
+ <CapRow className="rcs_text_area_wrapper">
969
+ {(isEditFlow || !isFullMode) ? (
970
+ renderCarouselEditMessage(card.description || '')
971
+ ) : (
972
+ <TextArea
973
+ autosize={{ minRows: 3, maxRows: 5 }}
974
+ value={card.description || ''}
975
+ placeholder={formatMessage(messages.templateDescPlaceholder)}
976
+ onChange={({ target: { value } }) => {
977
+ let error = false;
978
+ if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
979
+ error = formatMessage(messages.templateMessageLengthError);
980
+ } else {
981
+ error = variableErrorHandling(value);
982
+ }
983
+ updateCarouselErrors(index, { description: error });
984
+ handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
985
+ }}
986
+ disabled={isEditFlow || !isFullMode}
987
+ errorMessage={
988
+ carouselErrors?.[index]?.description && (
989
+ <CapError className="rcs-template-message-error">
990
+ {carouselErrors[index].description}
991
+ </CapError>
992
+ )
993
+ }
994
+ />
995
+ )}
996
+ </CapRow>
997
+ </CapRow>
998
+ {!isEditFlow && (
999
+ <CapRow className="rcs-carousel-character-count-row">
1000
+ {renderCarouselCharacterCount(
1001
+ getCarouselDescriptionCharacterCount(index),
1002
+ getDescriptionMaxLength(),
1003
+ )}
1004
+ </CapRow>
1005
+ )}
1006
+
1007
+ <CapDivider className="rcs-carousel-card-divider" />
1008
+ {renderCarouselCardButtons(index)}
1009
+ </CapCard>
1010
+ ),
1011
+ };
1012
+ });
1013
+ };
1014
+
1015
+ const renderCarouselSection = () => {
1016
+ if (!isCarouselType) return null;
1017
+
1018
+ const operations = (
1019
+ <>
1020
+ <CapDivider type="vertical" />
1021
+ <CapButton
1022
+ onClick={addCarouselCard}
1023
+ type="flat"
1024
+ className="add-carousel-content-button"
1025
+ disabled={
1026
+ isEditFlow ||
1027
+ !isFullMode ||
1028
+ MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0) ||
1029
+ checkDisableAddCarouselButton()
1030
+ }
1031
+ >
1032
+ <CapIcon type="plus" />
1033
+ </CapButton>
1034
+ </>
1035
+ );
1036
+
1037
+ return (
1038
+ <CapRow className="rcs-carousel-section">
1039
+ {renderCarouselDimensionSelection()}
1040
+ <CapRow className="rcs-carousel-tab">
1041
+ <CapTab
1042
+ key={`rcs-carousel-tab-${carouselResetNonce}`}
1043
+ defaultActiveKey="0"
1044
+ activeKey={activeCarouselIndex}
1045
+ tabBarExtraContent={operations}
1046
+ onChange={(key) => {
1047
+ setActiveCarouselIndex(`${key}`);
1048
+ // reset focused var when switching cards (as per requirement)
1049
+ setCarouselFocusedVarId('');
1050
+ }}
1051
+ panes={getCarouselTabPanes()}
1052
+ />
1053
+ </CapRow>
1054
+ </CapRow>
1055
+ );
1056
+ };
325
1057
 
326
1058
  const mediaRadioOptions = [
327
1059
  {
@@ -330,11 +1062,16 @@ export const Rcs = (props) => {
330
1062
  },
331
1063
  {
332
1064
  value: RCS_MEDIA_TYPES.VIDEO,
333
- label: formatMessage(messages.mediaVideo),
1065
+ label: formatMessage(
1066
+ templateType === contentType.carousel
1067
+ ? messages.carouselMediaVideoOption
1068
+ : messages.mediaVideo,
1069
+ ),
334
1070
  },
335
1071
  ];
336
1072
  const aiContentBotDisabled = isAiContentBotDisabled();
337
1073
 
1074
+
338
1075
  const updateButtonChange = (data, index) => {
339
1076
  if (data && data.text) {
340
1077
  const forbiddenError = forbiddenCharactersValidation(data.text);
@@ -371,7 +1108,9 @@ export const Rcs = (props) => {
371
1108
  if (isFullMode) return;
372
1109
  if (loadingTags || !tags || tags.length === 0) return;
373
1110
  const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
374
- const resolved = resolveTemplateWithMap(templateStr); // placeholders -> mapped value (or '')
1111
+ const slotOffset =
1112
+ type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
1113
+ const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
375
1114
  if (!resolved) {
376
1115
  if (type === TITLE_TEXT) setTemplateTitleError(false);
377
1116
  if (type === MESSAGE_TEXT) setTemplateDescError(false);
@@ -381,7 +1120,7 @@ export const Rcs = (props) => {
381
1120
  let contentForValidation = resolved;
382
1121
  const placeholderTokens = templateStr.match(rcsVarRegex) || [];
383
1122
  placeholderTokens.forEach((t) => {
384
- const escaped = t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1123
+ const escaped = t.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
385
1124
  contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
386
1125
  });
387
1126
  if (!contentForValidation.trim()) {
@@ -398,10 +1137,16 @@ export const Rcs = (props) => {
398
1137
  tagModule: getDefaultTags,
399
1138
  isFullMode,
400
1139
  }) || {};
401
- const errorMsg =
402
- (validationResponse?.isBraceError &&
403
- formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
404
- false;
1140
+ const unsupportedTagsLengthCheck =
1141
+ validationResponse?.unsupportedTags?.length > 0;
1142
+ const errorMsg =
1143
+ (unsupportedTagsLengthCheck &&
1144
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1145
+ unsupportedTags: validationResponse.unsupportedTags,
1146
+ })) ||
1147
+ (validationResponse.isBraceError &&
1148
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
1149
+ false;
405
1150
  if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
406
1151
  if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
407
1152
  };
@@ -414,15 +1159,98 @@ export const Rcs = (props) => {
414
1159
  validateResolvedTagsForType(MESSAGE_TEXT);
415
1160
  }, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
416
1161
 
1162
+ useEffect(() => {
1163
+ if (isFullMode || !isCarouselType) return;
1164
+ if (loadingTags || !tags || tags.length === 0) return;
1165
+ (carouselData || []).forEach((card, idx) => {
1166
+ ['title', 'description'].forEach((field) => {
1167
+ const templateStr = card?.[field] || '';
1168
+ if (!templateStr) return;
1169
+ const resolved = resolveTemplateWithMap(templateStr);
1170
+ if (!resolved) {
1171
+ updateCarouselErrors(idx, { [field]: false });
1172
+ return;
1173
+ }
1174
+ let contentForValidation = resolved;
1175
+ const placeholderTokens = templateStr.match(rcsVarRegex) || [];
1176
+ placeholderTokens.forEach((t) => {
1177
+ const escaped = t.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
1178
+ contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
1179
+ });
1180
+ if (!contentForValidation.trim()) {
1181
+ updateCarouselErrors(idx, { [field]: false });
1182
+ return;
1183
+ }
1184
+ const validationResponse = validateTags({
1185
+ content: contentForValidation,
1186
+ tagsParam: tags,
1187
+ injectedTagsParams: injectedTags,
1188
+ location,
1189
+ tagModule: getDefaultTags,
1190
+ eventContextTags,
1191
+ isFullMode,
1192
+ }) || {};
1193
+ const errorMsg =
1194
+ (validationResponse?.unsupportedTags?.length > 0 &&
1195
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
1196
+ unsupportedTags: validationResponse.unsupportedTags,
1197
+ })) ||
1198
+ (validationResponse.isBraceError &&
1199
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
1200
+ false;
1201
+ updateCarouselErrors(idx, { [field]: errorMsg });
1202
+ });
1203
+ });
1204
+ }, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
1205
+
417
1206
  const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
418
1207
 
419
- const resolveTemplateWithMap = (str = '') => {
1208
+ const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
1209
+
1210
+ /** Same `{{tag}}` in both title and description must not share one semantic map entry. */
1211
+ const rcsSpanningSemanticVarNames = useMemo(
1212
+ () => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
1213
+ [templateTitle, templateDesc],
1214
+ );
1215
+
1216
+ /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
1217
+ const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
1218
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
1219
+ const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
1220
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
1221
+ let varOrdinal = 0;
1222
+ for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
1223
+ const segmentToken = templateSegments[segmentIndexInField];
1224
+ if (rcsVarTestRegex.test(segmentToken)) {
1225
+ if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
1226
+ return offset + varOrdinal;
1227
+ }
1228
+ varOrdinal += 1;
1229
+ }
1230
+ }
1231
+ return null;
1232
+ };
1233
+
1234
+ /**
1235
+ * Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
1236
+ * multi-field variable resolution.
1237
+ */
1238
+ const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
420
1239
  if (!str) return '';
421
- const arr = splitTemplateVarString(str);
1240
+ const arr = splitTemplateVarStringRcs(str);
1241
+ let varOrdinal = 0;
422
1242
  return arr.map((elem) => {
423
1243
  if (rcsVarTestRegex.test(elem)) {
424
1244
  const key = getVarNameFromToken(elem);
425
- const v = cardVarMapped?.[key];
1245
+ const globalSlot = slotOffset + varOrdinal;
1246
+ varOrdinal += 1;
1247
+ const v = resolveCardVarMappedSlotValue(
1248
+ cardVarMapped,
1249
+ key,
1250
+ globalSlot,
1251
+ isEditLike,
1252
+ rcsSpanningSemanticVarNames.has(key),
1253
+ );
426
1254
  if (isNil(v) || String(v)?.trim?.() === '') return elem;
427
1255
  return String(v);
428
1256
  }
@@ -430,39 +1258,145 @@ export const Rcs = (props) => {
430
1258
  }).join('');
431
1259
  };
432
1260
 
433
-
434
- useEffect(() => {
435
- if (isFullMode || isEditFlow) return;
436
- const tokens = [
437
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
438
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
439
- ];
440
- if (!tokens.length) return;
441
- setCardVarMapped((prev) => {
442
- const next = { ...(prev || {}) };
443
- let changed = false;
444
- tokens.forEach((t) => {
445
- const name = getVarNameFromToken(t);
446
- if (name && !Object.prototype.hasOwnProperty.call(next, name)) {
447
- next[name] = '';
448
- changed = true;
449
- }
450
- });
451
- return changed ? next : prev;
1261
+ const buildCarouselCardsForPreview = (cards = []) => {
1262
+ // Track cumulative variable slot index across all cards so resolveTemplateWithMap
1263
+ // uses the correct globalSlot → slotKey for each token (e.g. card 1's {{3}} needs
1264
+ // slotOffset=2 so slotKey becomes "3", not "1").
1265
+ let slotOffset = 0;
1266
+ return (cards || []).map((card = {}) => {
1267
+ const rawTitle = card.title || '';
1268
+ const rawDesc = card.description || '';
1269
+ const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
1270
+ const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
1271
+ const resolvedTitle = !isFullMode
1272
+ ? resolveTemplateWithMap(rawTitle, slotOffset)
1273
+ : rawTitle;
1274
+ const resolvedDesc = !isFullMode
1275
+ ? resolveTemplateWithMap(rawDesc, slotOffset + titleVarCount)
1276
+ : rawDesc;
1277
+ if (!isFullMode) {
1278
+ slotOffset += titleVarCount + descVarCount;
1279
+ }
1280
+ const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
1281
+ const videoSrc = card.videoAsset?.videoSrc || '';
1282
+ return {
1283
+ mediaType: (card.mediaType || '').toLowerCase(),
1284
+ imageSrc: card.imageSrc || '',
1285
+ videoSrc,
1286
+ videoPreviewImg: videoThumb,
1287
+ title: resolvedTitle,
1288
+ bodyText: resolvedDesc,
1289
+ suggestions: card.suggestions || [],
1290
+ };
452
1291
  });
453
- }, [isFullMode, templateTitle, templateDesc]);
1292
+ };
1293
+
1294
+ /**
1295
+ * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
1296
+ * (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
1297
+ * TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
1298
+ */
1299
+ const getTemplateContent = useCallback(() => {
1300
+ if (templateType === contentType.carousel) {
1301
+ const carouselDimKey = getCarouselDimensionKey();
1302
+ const carouselImgDims =
1303
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
1304
+ const carouselVidDims =
1305
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
1306
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
1307
+ return {
1308
+ carouselData: buildCarouselCardsForPreview(carouselData),
1309
+ carouselPreviewDimensions: {
1310
+ imageWidth: carouselImgDims.width,
1311
+ imageHeight: carouselImgDims.height,
1312
+ videoThumbWidth: carouselVidDims.width,
1313
+ videoThumbHeight: carouselVidDims.height,
1314
+ },
1315
+ };
1316
+ }
1317
+ const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
1318
+ const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
1319
+ const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
1320
+
1321
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1322
+ const titleVarCountForResolve = isMediaTypeText
1323
+ ? 0
1324
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
1325
+ const resolvedTitle = isMediaTypeText
1326
+ ? ''
1327
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
1328
+ const resolvedDesc = isSlotMappingMode
1329
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
1330
+ : templateDesc;
1331
+
1332
+ const mediaPreview = {};
1333
+ if (isMediaTypeImage && rcsImageSrc) {
1334
+ mediaPreview.rcsImageSrc = rcsImageSrc;
1335
+ }
1336
+ if (isMediaTypeVideo && !isMediaTypeText) {
1337
+ if (rcsThumbnailSrc) {
1338
+ mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
1339
+ } else if (rcsVideoSrc?.videoSrc) {
1340
+ mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
1341
+ }
1342
+ if (rcsThumbnailSrc) {
1343
+ mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
1344
+ }
1345
+ }
1346
+
1347
+ const contentObj = {
1348
+ templateHeader: resolvedTitle,
1349
+ templateMessage: resolvedDesc,
1350
+ ...mediaPreview,
1351
+ ...(suggestions.length > 0 && {
1352
+ suggestions: suggestions,
1353
+ }),
1354
+ };
1355
+
1356
+ return contentObj;
1357
+ }, [
1358
+ templateMediaType,
1359
+ templateTitle,
1360
+ templateDesc,
1361
+ rcsImageSrc,
1362
+ rcsVideoSrc,
1363
+ rcsThumbnailSrc,
1364
+ suggestions,
1365
+ selectedDimension,
1366
+ isFullMode,
1367
+ isEditFlow,
1368
+ cardVarMapped,
1369
+ rcsSpanningSemanticVarNames,
1370
+ carouselData,
1371
+ selectedCarouselHeight,
1372
+ selectedCarouselWidth,
1373
+ ]);
454
1374
 
1375
+ const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
455
1376
 
456
- const RcsLabel = styled.div`
457
- display: flex;
458
- margin-top: 20px;
459
- `;
460
1377
  const paramObj = params || {};
461
1378
  useEffect(() => {
462
1379
  const { id } = paramObj;
463
1380
  if (id && isFullMode) {
464
1381
  setSpin(true);
465
1382
  actions.getTemplateDetails(id, setSpin);
1383
+ } else if (!id && isFullMode) {
1384
+ // Create New: clear standalone media and ALL possible carousel card Redux slots.
1385
+ // Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
1386
+ updateRcsImageSrc('');
1387
+ setRcsVideoSrc({});
1388
+ setRcsThumbnailSrc('');
1389
+ setAssetList({});
1390
+ setEditFlow(false);
1391
+ actions.clearRcsMediaAsset(0);
1392
+ actions.clearRcsMediaAsset(1);
1393
+ for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
1394
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
1395
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
1396
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
1397
+ }
1398
+ setCarouselData([]);
1399
+ setCarouselErrors([]);
466
1400
  }
467
1401
  return () => {
468
1402
  actions.clearEditResponse();
@@ -470,59 +1404,68 @@ export const Rcs = (props) => {
470
1404
  }, [paramObj.id]);
471
1405
 
472
1406
  useEffect(() => {
473
- if (!(isEditFlow || !isFullMode)) return;
1407
+ if (!(isEditFlow || !isFullMode)) return;
474
1408
 
475
- const initField = (targetString, currentVarMap, setVarMap, setUpdated) => {
476
- const arr = splitTemplateVarString(targetString);
477
- if (!arr?.length) {
478
- setVarMap({});
479
- setUpdated([]);
480
- return;
1409
+ const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
1410
+
1411
+ const initField = (targetString, setVarMap, slotOffset) => {
1412
+ const arr = splitTemplateVarStringRcs(targetString);
1413
+ if (!arr?.length) {
1414
+ setVarMap({});
1415
+ return;
1416
+ }
1417
+ const nextVarMap = {};
1418
+ let varOrdinal = 0;
1419
+ arr.forEach((elem, idx) => {
1420
+ // Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
1421
+ if (rcsVarTestRegex.test(elem)) {
1422
+ const id = `${elem}_${idx}`;
1423
+ const varName = getVarNameFromToken(elem);
1424
+ const globalSlot = slotOffset + varOrdinal;
1425
+ varOrdinal += 1;
1426
+ const mappedValue = resolveCardVarMappedSlotValue(
1427
+ cardVarMapped,
1428
+ varName,
1429
+ globalSlot,
1430
+ isEditLike,
1431
+ rcsSpanningSemanticVarNames.has(varName),
1432
+ );
1433
+ nextVarMap[id] = mappedValue;
481
1434
  }
482
- const nextVarMap = {};
483
- const nextUpdated = [...arr];
484
- arr.forEach((elem, idx) => {
485
- // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
486
- if (rcsVarTestRegex.test(elem)) {
487
- const id = `${elem}_${idx}`;
488
- const varName = getVarNameFromToken(elem);
489
- const mappedValue = (cardVarMapped?.[varName] ?? '').toString();
490
- nextVarMap[id] = mappedValue;
491
- if (mappedValue !== '') {
492
- nextUpdated[idx] = mappedValue;
493
- } else {
494
- nextUpdated[idx] = elem;
495
- }
496
- }
497
- });
498
- setVarMap(nextVarMap);
499
- setUpdated(nextUpdated);
500
- };
1435
+ });
1436
+ setVarMap(nextVarMap);
1437
+ };
501
1438
 
502
- initField(templateTitle, titleVarMappedData, setTitleVarMappedData, setUpdatedTitleData);
503
- initField(templateDesc, descVarMappedData, setDescVarMappedData, setUpdatedDescData);
504
- }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
505
-
506
- useEffect(() => {
507
- if(!isEditFlow && isFullMode){
1439
+ initField(templateTitle, setTitleVarMappedData, 0);
1440
+ initField(templateDesc, setDescVarMappedData, titleTokenCount);
1441
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
1442
+
1443
+ useEffect(() => {
1444
+ if (!isEditFlow && isFullMode) {
508
1445
  setRcsVideoSrc({});
509
1446
  updateRcsImageSrc('');
510
- setUpdateRcsImageSrc('');
511
- updateRcsThumbnailSrc('');
1447
+ setRcsThumbnailSrc('');
512
1448
  setAssetList({});
513
- }
514
- }, [templateMediaType]);
1449
+ }
1450
+ }, [templateMediaType]);
515
1451
 
516
- const templateStatusHelper = (details) => {
517
- const status = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
518
- switch (status) {
1452
+ /** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
1453
+ const templateStatusHelper = (cardContentFirst) => {
1454
+ const raw =
1455
+ cardContentFirst?.Status
1456
+ ?? cardContentFirst?.status
1457
+ ?? cardContentFirst?.approvalStatus
1458
+ ?? '';
1459
+ const status = typeof raw === 'string' ? raw.trim() : String(raw);
1460
+ const n = status.toLowerCase();
1461
+ switch (n) {
519
1462
  case RCS_STATUSES.approved:
520
1463
  setTemplateStatus(RCS_STATUSES.approved);
521
1464
  break;
522
1465
  case RCS_STATUSES.pending:
523
1466
  setTemplateStatus(RCS_STATUSES.pending);
524
1467
  break;
525
- case RCS_STATUSES.awaitingApproval:
1468
+ case RCS_STATUSES.awaitingApproval.toLowerCase():
526
1469
  setTemplateStatus(RCS_STATUSES.awaitingApproval);
527
1470
  break;
528
1471
  case RCS_STATUSES.unavailable:
@@ -531,6 +1474,9 @@ export const Rcs = (props) => {
531
1474
  case RCS_STATUSES.rejected:
532
1475
  setTemplateStatus(RCS_STATUSES.rejected);
533
1476
  break;
1477
+ case RCS_STATUSES.created:
1478
+ setTemplateStatus(RCS_STATUSES.created);
1479
+ break;
534
1480
  default:
535
1481
  setTemplateStatus(status);
536
1482
  break;
@@ -584,48 +1530,349 @@ export const Rcs = (props) => {
584
1530
  };
585
1531
 
586
1532
  useEffect(() => {
587
- const details = isFullMode ? rcsData?.templateDetails : templateData;
1533
+ const details = rcsHydrationDetails;
588
1534
  if (details && Object.keys(details).length > 0) {
589
- if (!isFullMode) {
590
- const tempCardVarMapped = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
591
- setCardVarMapped(tempCardVarMapped);
1535
+ // Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
1536
+ const cardFromVersions = get(
1537
+ details,
1538
+ 'versions.base.content.RCS.rcsContent.cardContent[0]',
1539
+ );
1540
+ const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
1541
+ const cardType = (rcsContent?.cardType || '').toString();
1542
+
1543
+ setEditFlow(true);
1544
+ setTemplateName(details?.name || details?.creativeName || '');
1545
+
1546
+ const cardFromTop = get(details, 'rcsContent.cardContent[0]');
1547
+ const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
1548
+ const cardVarMappedFromCardContent =
1549
+ card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
1550
+ ? card0.cardVarMapped
1551
+ : {};
1552
+ const cardVarMappedFromRootMirror =
1553
+ details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
1554
+ ? details.rcsCardVarMapped
1555
+ : {};
1556
+ // Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
1557
+ // nested versions.cardContent[0].cardVarMapped is dropped on reload.
1558
+ const mergedCardVarMappedFromPayload = {
1559
+ ...cardVarMappedFromRootMirror,
1560
+ ...cardVarMappedFromCardContent,
1561
+ };
1562
+ const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
1563
+ const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
1564
+ const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
1565
+ Object.keys(mergedCardVarMappedFromPayload)
1566
+ .sort()
1567
+ .reduce((accumulator, mapKey) => {
1568
+ accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
1569
+ return accumulator;
1570
+ }, {}),
1571
+ )}`;
1572
+ if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
1573
+ lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
1574
+ setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
592
1575
  }
593
- const mediaType = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
594
- if (mediaType === RCS_MEDIA_TYPES.NONE) {
1576
+ const tokenListForMap = [
1577
+ ...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
1578
+ ...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
1579
+ ];
1580
+ const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
1581
+ // Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
1582
+ // getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
1583
+ const cardVarMappedBeforeCoalesce = isFullMode
1584
+ ? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
1585
+ : { ...mergedCardVarMappedFromPayload };
1586
+ const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
1587
+ cardVarMappedBeforeCoalesce,
1588
+ loadedTitleForMap,
1589
+ loadedDescForMap,
1590
+ rcsVarRegex,
1591
+ );
1592
+ const cardVarMappedAfterNumericSlotSync = !isFullMode
1593
+ ? syncCardVarMappedSemanticsFromSlots(
1594
+ cardVarMappedAfterCoalesce,
1595
+ loadedTitleForMap,
1596
+ loadedDescForMap,
1597
+ rcsVarRegex,
1598
+ )
1599
+ : cardVarMappedAfterCoalesce;
1600
+ const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
1601
+ // Pre-populate variable/tag mappings while opening an existing template in edit flows
1602
+ setCardVarMapped((previousVarMapState) => {
1603
+ const previousVarMap = previousVarMapState ?? {};
1604
+ if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
1605
+ const previousVarMapKeys = Object.keys(previousVarMap);
1606
+ const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
1607
+ if (previousVarMapKeys.length === nextVarMapKeys.length) {
1608
+ const allSlotValuesMatchPrevious = previousVarMapKeys.every(
1609
+ (key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
1610
+ );
1611
+ if (allSlotValuesMatchPrevious) return previousVarMapState;
1612
+ }
1613
+ return hydratedCardVarMappedResult;
1614
+ });
1615
+
1616
+ if (cardType.toUpperCase() === contentType.carousel) {
1617
+
1618
+ setTemplateType(contentType.carousel);
1619
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1620
+ const cardSettings = rcsContent?.cardSettings || {};
1621
+ const cardWidth = cardSettings?.cardWidth || SMALL;
1622
+ setSelectedCarouselWidth(cardWidth);
1623
+
1624
+ const cards = Array.isArray(rcsContent?.cardContent) ? rcsContent.cardContent : [];
1625
+ const firstHeight = cards?.[0]?.media?.height || MEDIUM;
1626
+ setSelectedCarouselHeight(firstHeight);
1627
+ setSelectedCarousel(`${firstHeight}_${cardWidth}`);
1628
+ setActiveCarouselIndex('0');
1629
+
1630
+ const hydratedCards = cards.map((c = {}, idx) => {
1631
+ const mediaType = c.mediaType;
1632
+ const media = c.media || {};
1633
+ const mediaUrl = media.mediaUrl || '';
1634
+ const thumbUrl = media.thumbnailUrl || '';
1635
+ const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
1636
+ const suggestions = idx === 0 && rawSuggestions.length === 0
1637
+ ? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
1638
+ : rawSuggestions;
1639
+ return {
1640
+ title: c.title || '',
1641
+ description: c.description || '',
1642
+ mediaType,
1643
+ imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
1644
+ videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
1645
+ videoSrc: mediaUrl,
1646
+ previewUrl: thumbUrl,
1647
+ videoThumbnail: thumbUrl,
1648
+ videoName: c?.media?.videoName || '',
1649
+ } : {},
1650
+ thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
1651
+ suggestions,
1652
+ };
1653
+ });
1654
+ setCarouselData(
1655
+ hydratedCards.length > 0
1656
+ ? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
1657
+ : [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
1658
+ );
1659
+ setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
1660
+
1661
+ // Status bar uses first card's Status, keep existing behavior.
1662
+ if (isHostInfoBip) {
1663
+ setTemplateStatus('');
1664
+ } else {
1665
+ const firstCard = cards?.[0] || {};
1666
+ const cardForCarouselStatus = {
1667
+ ...firstCard,
1668
+ Status:
1669
+ firstCard.Status
1670
+ ?? firstCard.status
1671
+ ?? firstCard.approvalStatus
1672
+ ?? get(details, 'templateStatus')
1673
+ ?? get(details, 'approvalStatus')
1674
+ ?? get(details, 'creativeStatus')
1675
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1676
+ ?? '',
1677
+ };
1678
+ templateStatusHelper(cardForCarouselStatus);
1679
+ }
1680
+
1681
+ // Hydrate SMS fallback for carousel — same logic as for non-carousel types below.
1682
+ // Without this the early `return` skips fallback hydration entirely, so reopening
1683
+ // a carousel template never restores the saved SMS fallback data.
1684
+ const carouselSmsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
1685
+ const carouselSmsBase = get(carouselSmsFallbackContent, 'versions.base', {});
1686
+ const carouselUpdatedEditor = carouselSmsBase['updated-sms-editor'] ?? carouselSmsBase['sms-editor'];
1687
+ const carouselSmsEditor = carouselSmsBase['sms-editor'];
1688
+ const carouselFromNested = Array.isArray(carouselUpdatedEditor)
1689
+ ? carouselUpdatedEditor.join('')
1690
+ : (typeof carouselUpdatedEditor === 'string' ? carouselUpdatedEditor : (carouselSmsEditor || ''));
1691
+ const carouselFallbackMessage = carouselSmsFallbackContent.smsContent
1692
+ || carouselSmsFallbackContent.smsTemplateContent
1693
+ || carouselSmsFallbackContent.message
1694
+ || carouselFromNested
1695
+ || '';
1696
+ const carouselVarMappedFromPayload = carouselSmsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
1697
+ const carouselHasVarMapped = Object.keys(carouselVarMappedFromPayload).length > 0;
1698
+ const carouselHasFallbackPayload =
1699
+ carouselSmsFallbackContent
1700
+ && Object.keys(carouselSmsFallbackContent).length > 0
1701
+ && (
1702
+ !!carouselSmsFallbackContent.smsTemplateName
1703
+ || !!carouselFallbackMessage
1704
+ || carouselHasVarMapped
1705
+ );
1706
+ if (carouselHasFallbackPayload) {
1707
+ const carouselUnicodeFromApi =
1708
+ typeof carouselSmsFallbackContent.unicodeValidity === 'boolean'
1709
+ ? carouselSmsFallbackContent.unicodeValidity
1710
+ : (typeof carouselSmsBase['unicode-validity'] === 'boolean' ? carouselSmsBase['unicode-validity'] : true);
1711
+ const carouselRegisteredSenderIds =
1712
+ extractRegisteredSenderIdsFromSmsFallbackRecord(carouselSmsFallbackContent);
1713
+ const carouselNextSmsState = {
1714
+ templateName: carouselSmsFallbackContent.smsTemplateName || '',
1715
+ content: carouselFallbackMessage,
1716
+ templateContent: carouselFallbackMessage,
1717
+ unicodeValidity: carouselUnicodeFromApi,
1718
+ ...(carouselHasVarMapped && { rcsSmsFallbackVarMapped: carouselVarMappedFromPayload }),
1719
+ ...(Array.isArray(carouselRegisteredSenderIds) && carouselRegisteredSenderIds.length > 0
1720
+ ? { registeredSenderIds: carouselRegisteredSenderIds }
1721
+ : {}),
1722
+ };
1723
+ const carouselHydrationKey = JSON.stringify({
1724
+ creativeKey: details._id || details.name || details.creativeName || '',
1725
+ templateName: carouselNextSmsState.templateName,
1726
+ content: carouselNextSmsState.content,
1727
+ unicodeValidity: carouselNextSmsState.unicodeValidity,
1728
+ varMapped: carouselNextSmsState.rcsSmsFallbackVarMapped || {},
1729
+ senderIds: Array.isArray(carouselRegisteredSenderIds)
1730
+ ? carouselRegisteredSenderIds.join('')
1731
+ : '',
1732
+ });
1733
+ if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== carouselHydrationKey) {
1734
+ lastSmsFallbackHydrationKeyRef.current = carouselHydrationKey;
1735
+ setSmsFallbackData(carouselNextSmsState);
1736
+ }
1737
+ } else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
1738
+ lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
1739
+ setSmsFallbackData(null);
1740
+ }
1741
+
1742
+ return;
1743
+ }
1744
+
1745
+ const mediaType =
1746
+ card0.mediaType
1747
+ || get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
1748
+ if (cardType.toUpperCase() !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
595
1749
  setTemplateType(contentType.text_message);
596
- } else {
1750
+ } else if (cardType.toUpperCase() !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
597
1751
  setTemplateType(contentType.rich_card);
598
1752
  }
599
- setEditFlow(true);
600
- setTemplateName(details.name || '');
601
- const loadedTitle = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].title', '');
602
- const loadedDesc = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].description', '');
603
- const loadedMap = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
604
- const normalizedTitle = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedTitle, loadedMap) : loadedTitle;
605
- const normalizedDesc = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedDesc, loadedMap) : loadedDesc;
1753
+
1754
+ const loadedTitle = loadedTitleForMap;
1755
+ const loadedDesc = loadedDescForMap;
1756
+ const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
1757
+ loadedTitle,
1758
+ loadedDesc,
1759
+ isFullMode,
1760
+ cardVarMappedAfterHydration: hydratedCardVarMappedResult,
1761
+ rcsVarRegex,
1762
+ });
606
1763
  setTemplateTitle(normalizedTitle);
607
1764
  setTemplateDesc(normalizedDesc);
608
- setSuggestions(get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []));
609
- templateStatusHelper(details);
610
- const mediaData = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
611
- const cardSettings = get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '');
1765
+ setSuggestions(
1766
+ Array.isArray(card0.suggestions)
1767
+ ? card0.suggestions
1768
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
1769
+ );
1770
+ const cardForStatus = {
1771
+ ...card0,
1772
+ Status:
1773
+ card0.Status
1774
+ ?? card0.status
1775
+ ?? card0.approvalStatus
1776
+ ?? get(details, 'templateStatus')
1777
+ ?? get(details, 'approvalStatus')
1778
+ ?? get(details, 'creativeStatus')
1779
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1780
+ ?? '',
1781
+ };
1782
+ if (isHostInfoBip) {
1783
+ setTemplateStatus('');
1784
+ } else {
1785
+ templateStatusHelper(cardForStatus);
1786
+ }
1787
+ const mediaData =
1788
+ card0.media != null && card0.media !== ''
1789
+ ? card0.media
1790
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
1791
+ const cardSettings =
1792
+ get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
1793
+ || get(details, 'rcsContent.cardSettings', '');
612
1794
  setMediaData(mediaData, mediaType, cardSettings);
613
1795
  if (details?.edit) {
614
1796
  const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
615
1797
  setRcsAccount(rcsAccountId);
616
1798
  }
1799
+
1800
+ const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
1801
+ const base = get(smsFallbackContent, 'versions.base', {});
1802
+ const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
1803
+ const smsEditor = base['sms-editor'];
1804
+ const fromNested = Array.isArray(updatedEditor)
1805
+ ? updatedEditor.join('')
1806
+ : (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
1807
+ const fallbackMessage = smsFallbackContent.smsContent
1808
+ || smsFallbackContent.smsTemplateContent
1809
+ || smsFallbackContent.message
1810
+ || fromNested
1811
+ || '';
1812
+ const varMappedFromPayload = smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
1813
+ const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
1814
+ const hasFallbackPayload =
1815
+ smsFallbackContent
1816
+ && Object.keys(smsFallbackContent).length > 0
1817
+ && (
1818
+ !!smsFallbackContent.smsTemplateName
1819
+ || !!fallbackMessage
1820
+ || hasVarMapped
1821
+ );
1822
+ if (hasFallbackPayload) {
1823
+ if (!fallbackMessage && !hasVarMapped && process.env.NODE_ENV !== 'production') {
1824
+ console.warn('[RCS SMS Fallback] No message text found in API response. Inspect shape:', smsFallbackContent);
1825
+ }
1826
+ const unicodeFromApi =
1827
+ typeof smsFallbackContent.unicodeValidity === 'boolean'
1828
+ ? smsFallbackContent.unicodeValidity
1829
+ : (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
1830
+ const registeredSenderIdsFromApi =
1831
+ extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
1832
+ const nextSmsState = {
1833
+ templateName: smsFallbackContent.smsTemplateName || '',
1834
+ content: fallbackMessage,
1835
+ templateContent: fallbackMessage,
1836
+ unicodeValidity: unicodeFromApi,
1837
+ ...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
1838
+ ...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
1839
+ ? { registeredSenderIds: registeredSenderIdsFromApi }
1840
+ : {}),
1841
+ };
1842
+ const hydrationKey = JSON.stringify({
1843
+ creativeKey: details._id || details.name || details.creativeName || '',
1844
+ templateName: nextSmsState.templateName,
1845
+ content: nextSmsState.content,
1846
+ unicodeValidity: nextSmsState.unicodeValidity,
1847
+ varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
1848
+ senderIds:
1849
+ Array.isArray(registeredSenderIdsFromApi)
1850
+ ? registeredSenderIdsFromApi.join('\u001f')
1851
+ : '',
1852
+ });
1853
+ if (
1854
+ isFullMode
1855
+ || lastSmsFallbackHydrationKeyRef.current !== hydrationKey
1856
+ ) {
1857
+ lastSmsFallbackHydrationKeyRef.current = hydrationKey;
1858
+ setSmsFallbackData(nextSmsState);
1859
+ }
1860
+ } else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
1861
+ lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
1862
+ setSmsFallbackData(null);
1863
+ }
617
1864
  }
618
- }, [rcsData, templateData, isFullMode, isEditFlow]);
619
-
1865
+ }, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
620
1866
 
621
1867
  useEffect(() => {
622
1868
  if (templateType === contentType.text_message) {
623
1869
  setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
624
- setTemplateTitle('');
625
- setTemplateTitleError('');
1870
+ // Full-mode create only: switching to plain text clears draft title/media. Never clear when
1871
+ // hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
626
1872
  if (!isEditFlow && isFullMode) {
1873
+ setTemplateTitle('');
1874
+ setTemplateTitleError('');
627
1875
  setUpdateRcsImageSrc('');
628
- setUpdateRcsVideoSrc({});
629
1876
  setRcsVideoSrc({});
630
1877
  setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
631
1878
  }
@@ -652,7 +1899,8 @@ export const Rcs = (props) => {
652
1899
  if (!showDltContainer) {
653
1900
  const { type, module } = location.query || {};
654
1901
  const isEmbedded = type === EMBEDDED;
655
- const context = isEmbedded ? module : DEFAULT;
1902
+ // Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
1903
+ const context = isEmbedded ? module : 'outbound';
656
1904
  const embedded = isEmbedded ? type : FULL;
657
1905
  const query = {
658
1906
  layout: SMS,
@@ -663,9 +1911,9 @@ export const Rcs = (props) => {
663
1911
  if (getDefaultTags) {
664
1912
  query.context = getDefaultTags;
665
1913
  }
666
- globalActions.fetchSchemaForEntity(query);
1914
+ fetchTagSchemaIfNewQuery(query);
667
1915
  }
668
- }, [showDltContainer]);
1916
+ }, [showDltContainer, fetchTagSchemaIfNewQuery]);
669
1917
 
670
1918
  useEffect(() => {
671
1919
  let tag = get(metaEntities, `tags.standard`, []);
@@ -688,16 +1936,128 @@ export const Rcs = (props) => {
688
1936
  context,
689
1937
  embedded,
690
1938
  };
1939
+ if (getDefaultTags) {
1940
+ query.context = getDefaultTags;
1941
+ }
1942
+ fetchTagSchemaIfNewQuery(query);
691
1943
  globalActions.fetchSchemaForEntity(query);
692
1944
  };
693
1945
 
694
- const onTagSelect = (data, areaId) => {
695
- if (!areaId) return;
696
- const sep = areaId.lastIndexOf('_');
1946
+ const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
1947
+ if (!templateStr || !numericVarName || !tagName) return templateStr;
1948
+ const re = buildRcsNumericMustachePlaceholderRegex(numericVarName);
1949
+ return templateStr.replace(re, `{{${tagName}}}`);
1950
+ };
1951
+
1952
+ const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
1953
+ if (!varSegmentCompositeDomId) return;
1954
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
1955
+ if (underscoreIndexInCompositeId === -1) return;
1956
+ const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
1957
+ if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
1958
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
1959
+ const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
1960
+ if (!semanticOrNumericVarName) return;
1961
+ const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
1962
+ const templateStringForField =
1963
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
1964
+ const titleOrMessageFieldType =
1965
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
1966
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
1967
+ varSegmentCompositeDomId,
1968
+ templateStringForField,
1969
+ titleOrMessageFieldType,
1970
+ );
1971
+ const cardVarMappedNumericSlotKey =
1972
+ globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
1973
+ ? String(globalVarSlotIndexZeroBased + 1)
1974
+ : null;
1975
+
1976
+ setCardVarMapped((previousCardVarMapped) => {
1977
+ const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
1978
+ if (isNumericPlaceholderSlot) {
1979
+ const existingValueBeforeAppend = (
1980
+ previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
1981
+ ).toString();
1982
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1983
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1984
+ updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
1985
+ } else {
1986
+ // Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
1987
+ // "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
1988
+ // read/write the global numeric slot only and drop the shared semantic key.
1989
+ const existingValueBeforeAppend = cardVarMappedNumericSlotKey
1990
+ ? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
1991
+ : String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
1992
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1993
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1994
+ if (cardVarMappedNumericSlotKey) {
1995
+ updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
1996
+ // Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
1997
+ const fieldOffset = titleOrMessageFieldType === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
1998
+ const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
1999
+ let varOrdinal = 0;
2000
+ for (let i = 0; i < fieldSegments.length; i++) {
2001
+ if (rcsVarTestRegex.test(fieldSegments[i])) {
2002
+ const slotVarName = getVarNameFromToken(fieldSegments[i]);
2003
+ const globalSlot = fieldOffset + varOrdinal;
2004
+ varOrdinal += 1;
2005
+ if (slotVarName === semanticOrNumericVarName && globalSlot !== globalVarSlotIndexZeroBased) {
2006
+ updatedCardVarMapped[String(globalSlot + 1)] = mappedValueAfterAppendingTag;
2007
+ }
2008
+ }
2009
+ }
2010
+ } else {
2011
+ updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
2012
+ }
2013
+ }
2014
+ return updatedCardVarMapped;
2015
+ });
2016
+
2017
+ if (
2018
+ isNumericPlaceholderSlot
2019
+ && (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
2020
+ ) {
2021
+ if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
2022
+ setTemplateTitle((previousTitle) => {
2023
+ const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
2024
+ previousTitle || '',
2025
+ semanticOrNumericVarName,
2026
+ selectedTagNameFromPicker,
2027
+ );
2028
+ if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
2029
+ setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
2030
+ // Remount segment editor: tag insert replaces {{n}} with e.g. {{tag.FORMAT_1}} — slot ids change; avoids stale UI vs manual typing in full-mode TextArea
2031
+ setRcsVarSegmentEditorRemountKey((k) => k + 1);
2032
+ return titleAfterReplacingNumericPlaceholder;
2033
+ });
2034
+ } else {
2035
+ setTemplateDesc((previousDescription) => {
2036
+ const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
2037
+ previousDescription || '',
2038
+ semanticOrNumericVarName,
2039
+ selectedTagNameFromPicker,
2040
+ );
2041
+ if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
2042
+ return previousDescription;
2043
+ }
2044
+ setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
2045
+ setRcsVarSegmentEditorRemountKey((k) => k + 1);
2046
+ return descriptionAfterReplacingNumericPlaceholder;
2047
+ });
2048
+ }
2049
+ }
2050
+ };
2051
+
2052
+ const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
2053
+
2054
+ const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
2055
+
2056
+ const onCarouselTagSelect = (data) => {
2057
+ if (!carouselFocusedVarId) return;
2058
+ const sep = carouselFocusedVarId.lastIndexOf('_');
697
2059
  if (sep === -1) return;
698
- const numId = Number(areaId.slice(sep + 1));
699
- if (isNaN(numId)) return;
700
- const token = areaId.slice(0, sep);
2060
+ const token = carouselFocusedVarId.slice(0, sep);
701
2061
  const variableName = getVarNameFromToken(token);
702
2062
  if (!variableName) return;
703
2063
  setCardVarMapped((prev) => {
@@ -710,10 +2070,6 @@ export const Rcs = (props) => {
710
2070
  });
711
2071
  };
712
2072
 
713
- const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
714
-
715
- const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
716
-
717
2073
  const onTagSelectFallback = (data) => {
718
2074
  const tempMsg = `${fallbackMessage}{{${data}}}`;
719
2075
  const error = fallbackMessageErrorHandler(tempMsg);
@@ -733,15 +2089,14 @@ export const Rcs = (props) => {
733
2089
  };
734
2090
  // tag Code end
735
2091
 
736
- const renderLabel = (value, showLabel, desc) => {
737
- const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
2092
+ const renderLabel = (value, desc) => {
738
2093
  return (
739
2094
  <>
740
- <RcsLabel>
2095
+ <div className="rcs-form-section-heading">
741
2096
  <CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
742
- </RcsLabel>
2097
+ </div>
743
2098
  {desc && (
744
- <CapLabel type="label3" style={{ marginBottom: '17px' }}>
2099
+ <CapLabel type="label3" className="rcs-form-field-caption">
745
2100
  {formatMessage(messages[desc])}
746
2101
  </CapLabel>
747
2102
  )}
@@ -760,13 +2115,8 @@ export const Rcs = (props) => {
760
2115
  },
761
2116
  {
762
2117
  value: contentType.carousel,
763
- label: (
764
- <CapTooltip title={formatMessage(messages.disabledCarouselTooltip)}>
765
- {formatMessage(messages.carousel)}
766
- </CapTooltip>
767
- ),
768
- disabled: true,
769
- },
2118
+ label: formatMessage(messages.carousel),
2119
+ }
770
2120
  ];
771
2121
 
772
2122
  const onTemplateNameChange = ({ target: { value } }) => {
@@ -777,6 +2127,10 @@ export const Rcs = (props) => {
777
2127
 
778
2128
  const onTemplateTypeChange = ({ target: { value } }) => {
779
2129
  setTemplateType(value);
2130
+ // Carousel has per-card media; keep template-level media type neutral.
2131
+ if (value === contentType.carousel) {
2132
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
2133
+ }
780
2134
  };
781
2135
 
782
2136
 
@@ -797,8 +2151,39 @@ export const Rcs = (props) => {
797
2151
  const onTemplateMediaTypeChange = ({ target: { value } }) => {
798
2152
  setTemplateMediaType(value);
799
2153
  };
800
-
801
-
2154
+ const renderedRCSEditMessage = (descArray, type) => {
2155
+ const renderArray = [];
2156
+ if (descArray?.length) {
2157
+ descArray.forEach((elem, index) => {
2158
+ if (rcsVarTestRegex.test(elem)) {
2159
+ // Variable input
2160
+ renderArray.push(
2161
+ <TextArea
2162
+ id={`${elem}_${index}`}
2163
+ key={`${elem}_${index}`}
2164
+ placeholder={`enter the value for ${elem}`}
2165
+ autosize={{ minRows: 1, maxRows: 3 }}
2166
+ onChange={e => textAreaValueChange(e, type)}
2167
+ value={textAreaValue(index, type)}
2168
+ onFocus={(e) => setTextAreaId(e, type)}
2169
+ />
2170
+ );
2171
+ } else if (elem) {
2172
+ // Static text
2173
+ renderArray.push(
2174
+ <TextArea
2175
+ key={`static_${index}`}
2176
+ value={elem}
2177
+ autosize={{ minRows: 1, maxRows: 3 }}
2178
+ disabled
2179
+ className="rcs-edit-template-message-static-textarea"
2180
+ />
2181
+ );
2182
+ }
2183
+ });
2184
+ }
2185
+ return renderArray;
2186
+ };
802
2187
  const onTemplateTitleChange = ({ target: { value } }) => {
803
2188
  let errorMessage = false;
804
2189
  if (templateType === contentType.rich_card && !value.trim()) {
@@ -814,7 +2199,7 @@ export const Rcs = (props) => {
814
2199
 
815
2200
  const onTemplateDescChange = ({ target: { value } }) => {
816
2201
  let errorMessage = false;
817
- if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
2202
+ if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
818
2203
  errorMessage = formatMessage(messages.templateMessageLengthError);
819
2204
  } else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
820
2205
  errorMessage = formatMessage(messages.templateMessageLengthError);
@@ -830,16 +2215,16 @@ export const Rcs = (props) => {
830
2215
 
831
2216
  const templateDescErrorHandler = (value) => {
832
2217
  let errorMessage = false;
833
- const { isBraceError } = validateTags({
2218
+ const { unsupportedTags, isBraceError } = validateTags({
834
2219
  content: value,
835
2220
  tagsParam: tags,
2221
+ injectedTagsParams: injectedTags,
836
2222
  location,
837
2223
  tagModule: getDefaultTags,
838
- isFullMode,
839
2224
  }) || {};
840
2225
 
841
2226
  const maxLength = templateType === contentType.text_message
842
- ? RCS_TEXT_MESSAGE_MAX_LENGTH
2227
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
843
2228
  : RCS_RICH_CARD_MAX_LENGTH;
844
2229
 
845
2230
  if (value === '' && isMediaTypeText) {
@@ -862,10 +2247,25 @@ export const Rcs = (props) => {
862
2247
  };
863
2248
 
864
2249
  const fallbackMessageErrorHandler = (value) => {
2250
+ let errorMessage = false;
2251
+ const { unsupportedTags } = validateTags({
2252
+ content: value,
2253
+ tagsParam: tags,
2254
+ injectedTagsParams: injectedTags,
2255
+ location,
2256
+ tagModule: getDefaultTags,
2257
+ }) || {};
865
2258
  if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
866
- return formatMessage(messages.fallbackMsgLenError);
2259
+ errorMessage = formatMessage(messages.fallbackMsgLenError);
2260
+ } else if (unsupportedTags?.length > 0) {
2261
+ errorMessage = formatMessage(
2262
+ globalMessages.unsupportedTagsValidationError,
2263
+ {
2264
+ unsupportedTags,
2265
+ },
2266
+ );
867
2267
  }
868
- return false;
2268
+ return errorMessage;
869
2269
  };
870
2270
 
871
2271
  // Check for forbidden characters: square brackets [] and single curly braces {}
@@ -912,53 +2312,43 @@ export const Rcs = (props) => {
912
2312
  if(!isFullMode){
913
2313
  return false;
914
2314
  }
915
- if (!/^\w+$/.test(paramName)) {
2315
+ // Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
2316
+ if (!/^[\w.]+$/.test(paramName)) {
916
2317
  return formatMessage(messages.unknownCharactersError);
917
2318
  }
918
2319
  }
919
2320
  return false;
920
2321
  };
921
-
922
- const templateHeaderErrorHandler = (value) => {
923
- let errorMessage = false;
924
- if (value?.length > TEMPLATE_HEADER_MAX_LENGTH) {
925
- errorMessage = formatMessage(messages.templateHeaderLengthError);
926
- } else {
927
- errorMessage = variableErrorHandling(value);
928
- }
929
- return errorMessage;
930
- };
931
-
932
-
933
- const templateMessageErrorHandler = (value) => {
934
- let errorMessage = false;
935
- if (value === '') {
936
- errorMessage = formatMessage(messages.emptyTemplateMessageErrorMessage);
937
- } else if (
938
- value?.length
939
- > TEMPLATE_MESSAGE_MAX_LENGTH
940
- ) {
941
- errorMessage = formatMessage(messages.templateMessageLengthError);
942
- } else {
943
- errorMessage = variableErrorHandling(value);
944
- }
945
- return errorMessage;
946
- };
947
-
948
2322
 
949
2323
  const onMessageAddVar = () => {
950
- onAddVar(MESSAGE_TEXT, templateDesc, rcsVarRegex);
2324
+ onAddVar(templateDesc);
951
2325
  };
952
2326
 
953
- const onAddVar = (type, messageContent, regex) => {
954
- // Always append the next variable at the end, like WhatsApp
955
- const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
956
- const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
2327
+ /**
2328
+ * Returns the smallest positive integer not already used as a `{{N}}` variable
2329
+ * in either the title or description fields, or null if the limit (19) is reached.
2330
+ * Scans both fields so title and description vars never share the same number
2331
+ * (duplicate numbers would share a cardVarMapped key and bleed values across fields).
2332
+ */
2333
+ const getNextRcsNumericVarNumber = (titleStr, descStr) => {
2334
+ const allExistingVars = [
2335
+ ...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
2336
+ ...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
2337
+ ];
2338
+ const existingNumbers = allExistingVars.flatMap(v => {
2339
+ const m = v.match(/\d+/);
2340
+ return m ? [parseInt(m[0], 10)] : [];
2341
+ });
957
2342
  let nextNumber = 1;
958
2343
  while (existingNumbers.includes(nextNumber)) {
959
2344
  nextNumber++;
960
2345
  }
961
- if (nextNumber > 19) {
2346
+ return nextNumber > 19 ? null : nextNumber;
2347
+ };
2348
+
2349
+ const onAddVar = (messageContent) => {
2350
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
2351
+ if (nextNumber === null) {
962
2352
  return;
963
2353
  }
964
2354
  const nextVar = `{{${nextNumber}}}`;
@@ -970,15 +2360,13 @@ const onAddVar = (type, messageContent, regex) => {
970
2360
  };
971
2361
 
972
2362
  const onTitleAddVar = () => {
973
- // Always append the next variable at the end, like WhatsApp
2363
+ // Scan both title AND description so the new title var number doesn't
2364
+ // duplicate a number already used in the description. Duplicate numeric
2365
+ // names would share the same cardVarMapped semantic key, causing the
2366
+ // description slot to reflect the title slot value and vice-versa.
974
2367
  const messageContent = templateTitle;
975
- const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
976
- const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
977
- let nextNumber = 1;
978
- while (existingNumbers.includes(nextNumber)) {
979
- nextNumber++;
980
- }
981
- if (nextNumber > 19) {
2368
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
2369
+ if (nextNumber === null) {
982
2370
  return;
983
2371
  }
984
2372
  const nextVar = `{{${nextNumber}}}`;
@@ -990,26 +2378,35 @@ const onTitleAddVar = () => {
990
2378
  setTemplateTitleError(error);
991
2379
  };
992
2380
 
993
-
994
- const splitTemplateVarString = (str) => {
995
- if (!str) return [];
996
- const validVarArr = str.match(rcsVarRegex) || [];
997
- const templateVarArray = [];
998
- let content = str;
999
- while (content?.length !== 0) {
1000
- const index = content.indexOf(validVarArr?.[0]);
1001
- if (index !== -1) {
1002
- templateVarArray.push(content.substring(0, index));
1003
- templateVarArray.push(validVarArr?.[0]);
1004
- content = content.substring(index + validVarArr?.[0]?.length, content?.length);
1005
- validVarArr?.shift();
1006
- } else {
1007
- templateVarArray.push(content);
1008
- break;
1009
- }
1010
- }
1011
- return templateVarArray.filter(Boolean);
1012
- };
2381
+ // Carousel: global variables across the whole carousel (all cards, title+body)
2382
+ const getNextCarouselVarToken = () => {
2383
+ const nums = [];
2384
+ (carouselData || []).forEach((c = {}) => {
2385
+ const titleMatches = (c.title || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
2386
+ const descMatches = (c.description || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
2387
+ [...titleMatches, ...descMatches].forEach((tok) => {
2388
+ const n = parseInt((tok.match(/\d+/) || [])[0], 10);
2389
+ if (!Number.isNaN(n)) nums.push(n);
2390
+ });
2391
+ });
2392
+ const existing = new Set(nums);
2393
+ let nextNumber = 1;
2394
+ while (existing.has(nextNumber)) nextNumber++;
2395
+ if (nextNumber > 19) return '';
2396
+ return `{{${nextNumber}}}`;
2397
+ };
2398
+
2399
+ const appendVarToCarouselField = (cardIndex, fieldName) => {
2400
+ const token = getNextCarouselVarToken();
2401
+ if (!token) return;
2402
+ setCarouselData((prev = []) => {
2403
+ const updated = cloneDeep(prev);
2404
+ if (!updated[cardIndex]) return prev;
2405
+ const current = (updated[cardIndex][fieldName] || '').toString();
2406
+ updated[cardIndex][fieldName] = `${current}${token}`;
2407
+ return updated;
2408
+ });
2409
+ };
1013
2410
 
1014
2411
  const textAreaValue = (idValue, type) => {
1015
2412
  if (idValue >= 0) {
@@ -1025,6 +2422,46 @@ const splitTemplateVarString = (str) => {
1025
2422
  return "";
1026
2423
  };
1027
2424
 
2425
+ // Carousel: render variable-value editor for a given template string (title/description).
2426
+ // This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
2427
+ const renderCarouselEditMessage = (templateStr) => {
2428
+ const renderArray = [];
2429
+ const templateArr = splitTemplateVarString(templateStr);
2430
+ if (templateArr?.length) {
2431
+ templateArr.forEach((elem, index) => {
2432
+ if (rcsVarTestRegex.test(elem)) {
2433
+ const varName = getVarNameFromToken(elem);
2434
+ renderArray.push(
2435
+ <div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
2436
+ <TextArea
2437
+ id={`${elem}_${index}`}
2438
+ placeholder={`enter the value for ${elem}`}
2439
+ autosize={{ minRows: 1, maxRows: 3 }}
2440
+ onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
2441
+ value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
2442
+ onFocus={(e) => {
2443
+ const id = e?.target?.id || e?.currentTarget?.id || '';
2444
+ setCarouselFocusedVarId(id);
2445
+ }}
2446
+ />
2447
+ </div>
2448
+ );
2449
+ } else if (elem) {
2450
+ renderArray.push(
2451
+ <CapHeading
2452
+ key={`static_${index}`}
2453
+ type="h4"
2454
+ className="rcs-edit-template-message-split"
2455
+ >
2456
+ {elem}
2457
+ </CapHeading>
2458
+ );
2459
+ }
2460
+ });
2461
+ }
2462
+ return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
2463
+ };
2464
+
1028
2465
  const textAreaValueChange = (e, type) => {
1029
2466
  const value = e?.target?.value ?? '';
1030
2467
  const id = e?.target?.id || e?.currentTarget?.id || '';
@@ -1044,7 +2481,9 @@ const splitTemplateVarString = (str) => {
1044
2481
  };
1045
2482
 
1046
2483
  const setTextAreaId = (e, type) => {
1047
- const id = e?.target?.id || e?.currentTarget?.id || '';
2484
+ // VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
2485
+ // have an `.target.id` shape. Support both.
2486
+ const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
1048
2487
  if (!id) return;
1049
2488
  if (type === TITLE_TEXT) setTitleTextAreaId(id);
1050
2489
  else setDescTextAreaId(id);
@@ -1075,44 +2514,85 @@ const splitTemplateVarString = (str) => {
1075
2514
  isEditFlow={isEditFlow}
1076
2515
  isFullMode={isFullMode}
1077
2516
  maxButtons={MAX_BUTTONS}
1078
- />
2517
+ host={hostName}
2518
+ />
1079
2519
  </>
1080
2520
  );
1081
2521
  };
1082
2522
 
1083
- const renderedRCSEditMessage = (descArray, type) => {
1084
- const renderArray = [];
1085
- if (descArray?.length) {
1086
- descArray.forEach((elem, index) => {
1087
- if (rcsVarTestRegex.test(elem)) {
1088
- // Variable input
1089
- renderArray.push(
1090
- <TextArea
1091
- id={`${elem}_${index}`}
1092
- key={`${elem}_${index}`}
1093
- placeholder={`enter the value for ${elem}`}
1094
- autosize={{ minRows: 1, maxRows: 3 }}
1095
- onChange={e => textAreaValueChange(e, type)}
1096
- value={textAreaValue(index, type)}
1097
- onFocus={(e) => setTextAreaId(e, type)}
1098
- />
1099
- );
1100
- } else if (elem) {
1101
- // Static text
1102
- renderArray.push(
1103
- <TextArea
1104
- key={`static_${index}`}
1105
- value={elem}
1106
- autosize={{ minRows: 1, maxRows: 3 }}
1107
- disabled
1108
- className="rcs-edit-template-message-static-textarea"
1109
- style={{ background: '#fafafa', color: '#888' }}
1110
- />
1111
- );
2523
+ const getRcsValueMap = (fieldTemplateString, fieldType) => {
2524
+ if (!fieldTemplateString) return {};
2525
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
2526
+ const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
2527
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
2528
+ const segmentIdToResolvedValue = {};
2529
+ let varOrdinal = 0;
2530
+ templateSegments.forEach((segmentToken, segmentIndexInField) => {
2531
+ if (rcsVarTestRegex.test(segmentToken)) {
2532
+ const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
2533
+ const varName = getVarNameFromToken(segmentToken);
2534
+ const globalSlot = slotOffset + varOrdinal;
2535
+ varOrdinal += 1;
2536
+ segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
2537
+ cardVarMapped,
2538
+ varName,
2539
+ globalSlot,
2540
+ isEditLike,
2541
+ rcsSpanningSemanticVarNames.has(varName),
2542
+ );
2543
+ }
2544
+ });
2545
+ return segmentIdToResolvedValue;
2546
+ };
2547
+
2548
+ const titleVarSegmentValueMapById = useMemo(
2549
+ () => getRcsValueMap(templateTitle, TITLE_TEXT),
2550
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
2551
+ );
2552
+ const descriptionVarSegmentValueMapById = useMemo(
2553
+ () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
2554
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
2555
+ );
2556
+
2557
+ const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
2558
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
2559
+ if (underscoreIndexInCompositeId === -1) return;
2560
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
2561
+ const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
2562
+ if (variableName === undefined || variableName === null || variableName === '') return;
2563
+ const isInvalidValue = value?.trim() === '';
2564
+ const coercedSlotValue = isInvalidValue ? '' : value;
2565
+ const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
2566
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
2567
+ varSegmentCompositeDomId,
2568
+ templateStringForField,
2569
+ type,
2570
+ );
2571
+ const fieldOffset = type === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
2572
+ setCardVarMapped((previousCardVarMapped) => {
2573
+ const updatedCardVarMapped = { ...previousCardVarMapped };
2574
+ // Remove stale semantic key: keeping it causes every other slot sharing the same
2575
+ // variable name (e.g. {{adv}} in both title and description) to read the same value
2576
+ // via the semantic-key fallback in resolveCardVarMappedSlotValue.
2577
+ delete updatedCardVarMapped[variableName];
2578
+ if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
2579
+ updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
2580
+ // Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
2581
+ const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
2582
+ let varOrdinal = 0;
2583
+ for (let i = 0; i < fieldSegments.length; i++) {
2584
+ if (rcsVarTestRegex.test(fieldSegments[i])) {
2585
+ const slotVarName = getVarNameFromToken(fieldSegments[i]);
2586
+ const globalSlot = fieldOffset + varOrdinal;
2587
+ varOrdinal += 1;
2588
+ if (slotVarName === variableName && globalSlot !== globalVarSlotIndexZeroBased) {
2589
+ updatedCardVarMapped[String(globalSlot + 1)] = coercedSlotValue;
2590
+ }
2591
+ }
1112
2592
  }
1113
- });
1114
- }
1115
- return renderArray;
2593
+ }
2594
+ return updatedCardVarMapped;
2595
+ });
1116
2596
  };
1117
2597
 
1118
2598
  const renderTextComponent = () => {
@@ -1132,6 +2612,7 @@ const splitTemplateVarString = (str) => {
1132
2612
  }
1133
2613
  suffix={
1134
2614
  <>
2615
+
1135
2616
  {(isEditFlow || !isFullMode) ? (
1136
2617
  <TagList
1137
2618
  label={formatMessage(globalMessages.addLabels)}
@@ -1148,18 +2629,28 @@ const splitTemplateVarString = (str) => {
1148
2629
  type="flat"
1149
2630
  isAddBtn
1150
2631
  onClick={onTitleAddVar}
1151
- disabled={!!templateTitleError}
2632
+ disabled={templateTitleError}
1152
2633
  >
1153
2634
  {formatMessage(messages.addVar)}
1154
2635
  </CapButton>
1155
- )}
2636
+ )}
1156
2637
  </>
1157
- }
2638
+ }
2639
+ />
2640
+
2641
+ {(isEditFlow || !isFullMode) ? (
2642
+ <VarSegmentMessageEditor
2643
+ key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
2644
+ templateString={templateTitle}
2645
+ valueMap={titleVarSegmentValueMapById}
2646
+ onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
2647
+ onFocus={(id) => setTitleTextAreaId(id)}
2648
+ varRegex={rcsVarRegex}
2649
+ placeholderPrefix=""
2650
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
1158
2651
  />
1159
- <div className="rcs_text_area_wrapper">
1160
- {(isEditFlow || !isFullMode) ? (
1161
- renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
1162
2652
  ) : (
2653
+ <div className="rcs_text_area_wrapper">
1163
2654
  <CapInput
1164
2655
  className={`rcs-template-title-input ${
1165
2656
  !isTemplateApproved ? "rcs-edit-disabled" : ""
@@ -1172,8 +2663,8 @@ const splitTemplateVarString = (str) => {
1172
2663
  errorMessage={templateTitleError}
1173
2664
  disabled={isEditFlow || !isFullMode}
1174
2665
  />
2666
+ </div>
1175
2667
  )}
1176
- </div>
1177
2668
  {(isEditFlow || !isFullMode) && templateTitleError && (
1178
2669
  <CapError className="rcs-template-title-error">
1179
2670
  {templateTitleError}
@@ -1181,7 +2672,7 @@ const splitTemplateVarString = (str) => {
1181
2672
  )}
1182
2673
  {!isEditFlow && isFullMode && renderTitleCharacterCount()}
1183
2674
  </>
1184
- )}
2675
+ )}
1185
2676
 
1186
2677
  {/* Template Message */}
1187
2678
  <CapRow useLegacy id="rcs-template-message-label">
@@ -1219,9 +2710,21 @@ const splitTemplateVarString = (str) => {
1219
2710
  />
1220
2711
  </CapRow>
1221
2712
  <CapRow useLegacy className="rcs-create-template-message-input">
1222
- <div className="rcs_text_area_wrapper">
1223
- {(isEditFlow || !isFullMode)
1224
- ? renderedRCSEditMessage(splitTemplateVarString(templateDesc), MESSAGE_TEXT)
2713
+ {/* Edit/library: segmented inputs (split on {{…}}). Full-mode create: single TextArea below — manual entry there never hits segment split. TagList replaces {{n}} in template string here. */}
2714
+ <CapRow className="rcs_text_area_wrapper">
2715
+ {(isEditFlow || !isFullMode)?
2716
+ (
2717
+ <VarSegmentMessageEditor
2718
+ key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
2719
+ templateString={templateDesc}
2720
+ valueMap={descriptionVarSegmentValueMapById}
2721
+ onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
2722
+ onFocus={(id) => setDescTextAreaId(id)}
2723
+ varRegex={rcsVarRegex}
2724
+ placeholderPrefix=""
2725
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
2726
+ />
2727
+ )
1225
2728
  : (
1226
2729
  <>
1227
2730
  <TextArea
@@ -1259,13 +2762,15 @@ const splitTemplateVarString = (str) => {
1259
2762
  </>
1260
2763
  )
1261
2764
  }
1262
- </div>
2765
+ </CapRow>
1263
2766
  {(isEditFlow || !isFullMode) && templateDescError && (
1264
2767
  <CapError className="rcs-template-message-error">
1265
2768
  {templateDescError}
1266
2769
  </CapError>
1267
2770
  )}
1268
- {!isEditFlow && isFullMode && renderDescriptionCharacterCount()}
2771
+ {(isEditFlow || !isFullMode)
2772
+ ? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
2773
+ : (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
1269
2774
  {!isFullMode && hasTag() && (
1270
2775
  <CapAlert
1271
2776
  message={
@@ -1279,24 +2784,13 @@ const splitTemplateVarString = (str) => {
1279
2784
  />
1280
2785
  )}
1281
2786
  </CapRow>
1282
- {renderButtonComponent()}
2787
+ {((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
2788
+ renderButtonComponent()}
1283
2789
  </>
1284
2790
 
1285
2791
  );
1286
2792
  };
1287
2793
 
1288
-
1289
- const fallbackSmsLength = () => (
1290
- <CapLabel type="label1" className="fallback-sms-length">
1291
- {formatMessage(messages.totalCharacters, {
1292
- smsCount: Math.ceil(
1293
- fallbackMessage?.length / FALLBACK_MESSAGE_MAX_LENGTH,
1294
- ),
1295
- number: fallbackMessage?.length,
1296
- })}
1297
- </CapLabel>
1298
- );
1299
-
1300
2794
  // Get character count for title (rich card only)
1301
2795
  const getTitleCharacterCount = () => {
1302
2796
  if (templateType === contentType.text_message) return 0;
@@ -1311,7 +2805,7 @@ const splitTemplateVarString = (str) => {
1311
2805
  // Get max length for description based on template type
1312
2806
  const getDescriptionMaxLength = () => {
1313
2807
  return templateType === contentType.text_message
1314
- ? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
2808
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
1315
2809
  : RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
1316
2810
  };
1317
2811
 
@@ -1337,6 +2831,58 @@ const splitTemplateVarString = (str) => {
1337
2831
  );
1338
2832
  };
1339
2833
 
2834
+ const rcsDltCardDeleteHandler = () => {
2835
+ closeDltContainerHandler();
2836
+ setDltEditData({});
2837
+ setFallbackMessage('');
2838
+ setFallbackMessageError(false);
2839
+ };
2840
+
2841
+ const dltFallbackListingPreviewhandler = (data) => {
2842
+ const {
2843
+ 'updated-sms-editor': updatedSmsEditor = [],
2844
+ 'sms-editor': smsEditor = '',
2845
+ } = data.versions.base || {};
2846
+ };
2847
+
2848
+ const getDltContentCardList = (content, channel) => {
2849
+ const extra = [
2850
+ <CapIcon
2851
+ type="edit"
2852
+ className="rcs-dlt-edit-icon"
2853
+ onClick={() => rcsDltEditSelectHandler(dltEditData)}
2854
+ />,
2855
+ <CapDropdown
2856
+ overlay={(
2857
+ <CapMenu>
2858
+ <>
2859
+ <CapMenu.Item
2860
+ className="ant-dropdown-menu-item"
2861
+ onClick={() => {}}
2862
+ >
2863
+ {formatMessage(globalMessages.preview)}
2864
+ </CapMenu.Item>
2865
+ <CapMenu.Item
2866
+ className="ant-dropdown-menu-item"
2867
+ onClick={rcsDltCardDeleteHandler}
2868
+ >
2869
+ {formatMessage(globalMessages.delete)}
2870
+ </CapMenu.Item>
2871
+ </>
2872
+ </CapMenu>
2873
+ )}
2874
+ >
2875
+ <CapIcon type="more" />
2876
+ </CapDropdown>,
2877
+ ];
2878
+ return {
2879
+ title: channel,
2880
+ content,
2881
+ cardType: channel,
2882
+ extra,
2883
+ };
2884
+ };
2885
+
1340
2886
  // Render character count for description/message
1341
2887
  const renderDescriptionCharacterCount = (className = "rcs-character-count") => {
1342
2888
  const currentLength = getDescriptionCharacterCount();
@@ -1352,6 +2898,26 @@ const splitTemplateVarString = (str) => {
1352
2898
  );
1353
2899
  };
1354
2900
 
2901
+ // Carousel: per-card character counts (same limits as rich card)
2902
+ const getCarouselTitleCharacterCount = (cardIndex) => {
2903
+ const titleText = carouselData?.[cardIndex]?.title || '';
2904
+ return titleText ? titleText.length : 0;
2905
+ };
2906
+
2907
+ const getCarouselDescriptionCharacterCount = (cardIndex) => {
2908
+ const descriptionText = carouselData?.[cardIndex]?.description || '';
2909
+ return descriptionText ? descriptionText.length : 0;
2910
+ };
2911
+
2912
+ const renderCarouselCharacterCount = (currentLength, maxLength, className = "rcs-character-count") => (
2913
+ <CapLabel type="label1" className={className}>
2914
+ {formatMessage(messages.templateMessageLength, {
2915
+ currentLength,
2916
+ maxLength,
2917
+ })}
2918
+ </CapLabel>
2919
+ );
2920
+
1355
2921
  // Check if any RCS variables contain tags (similar to Zalo hasTag logic)
1356
2922
  const hasTag = () => {
1357
2923
  // Check cardVarMapped values for tags
@@ -1404,68 +2970,17 @@ const splitTemplateVarString = (str) => {
1404
2970
  const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
1405
2971
  '',
1406
2972
  );
2973
+ const templateNameFromDlt = get(dltEditData, 'name', '')
2974
+ || get(tempData, 'versions.base.name', '')
2975
+ || '';
1407
2976
  closeDltContainerHandler();
1408
2977
  setDltEditData(tempData);
1409
- setFallbackMessage(fallMsg);
1410
- setFallbackMessageError(fallMsg?.length > FALLBACK_MESSAGE_MAX_LENGTH);
1411
- setShowDltCard(true);
1412
- };
1413
-
1414
- const rcsDltCardDeleteHandler = () => {
1415
- closeDltContainerHandler();
1416
- setDltEditData({});
1417
- setFallbackMessage('');
1418
- setFallbackMessageError(false);
1419
- setShowDltCard(false);
1420
- };
1421
-
1422
- const dltFallbackListingPreviewhandler = (data) => {
1423
- const {
1424
- 'updated-sms-editor': updatedSmsEditor = [],
1425
- 'sms-editor': smsEditor = '',
1426
- } = data.versions.base || {};
1427
- setFallbackPreviewmode(true);
1428
- setDltPreviewData(
1429
- updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
1430
- );
1431
- };
1432
-
1433
- const getDltContentCardList = (content, channel) => {
1434
- const extra = [
1435
- <CapIcon
1436
- type="edit"
1437
- style={{ marginRight: '8px' }}
1438
- onClick={() => rcsDltEditSelectHandler(dltEditData)}
1439
- />,
1440
- <CapDropdown
1441
- overlay={(
1442
- <CapMenu>
1443
- <>
1444
- <CapMenu.Item
1445
- className="ant-dropdown-menu-item"
1446
- onClick={() => setFallbackPreviewmode(true)}
1447
- >
1448
- {formatMessage(globalMessages.preview)}
1449
- </CapMenu.Item>
1450
- <CapMenu.Item
1451
- className="ant-dropdown-menu-item"
1452
- onClick={rcsDltCardDeleteHandler}
1453
- >
1454
- {formatMessage(globalMessages.delete)}
1455
- </CapMenu.Item>
1456
- </>
1457
- </CapMenu>
1458
- )}
1459
- >
1460
- <CapIcon type="more" />
1461
- </CapDropdown>,
1462
- ];
1463
- return {
1464
- title: channel,
1465
- content,
1466
- cardType: channel,
1467
- extra,
1468
- };
2978
+ const unicodeFromDlt = get(tempData, 'versions.base.unicode-validity');
2979
+ setSmsFallbackData({
2980
+ templateName: templateNameFromDlt,
2981
+ content: fallMsg,
2982
+ ...(typeof unicodeFromDlt === 'boolean' ? { unicodeValidity: unicodeFromDlt } : {}),
2983
+ });
1469
2984
  };
1470
2985
 
1471
2986
  const getDltSlideBoxContent = () => {
@@ -1513,148 +3028,34 @@ const splitTemplateVarString = (str) => {
1513
3028
  return { dltHeader, dltContent };
1514
3029
  };
1515
3030
 
1516
- const renderFallBackSmsComponent = () => {
1517
- // Completely disable fallback functionality when DLT is disabled
1518
- return null;
1519
-
1520
- // const contentArr = [];
1521
- // const showAddCreativeBtnForDlt = isDltEnabled && !showDltCard;
1522
- // const showCardForDlt = isDltEnabled && showDltCard;
1523
- // const showNonDltFallbackComp = !showAddCreativeBtnForDlt && !showCardForDlt;
1524
- // //pushing common fallback sms headings
1525
- // contentArr.push(
1526
- // <CapRow
1527
- // style={{
1528
- // marginBottom: isDltEnabled ? '20px' : '10px',
1529
- // }}
1530
- // >
1531
- // <CapHeader
1532
- // title={(
1533
- // <CapRow type="flex">
1534
- // <CapHeading type="h4">
1535
- // {formatMessage(messages.fallbackLabel)}
1536
- // </CapHeading>
1537
- // <CapTooltipWithInfo
1538
- // placement="right"
1539
- // infoIconProps={{
1540
- // style: { marginLeft: '4px', marginTop: '3px' },
1541
- // }}
1542
- // title={formatMessage(messages.fallbackToolTip)}
1543
- // />
1544
- // </CapRow>
1545
- // )}
1546
- // description={formatMessage(messages.fallbackDesc)}
1547
- // suffix={
1548
- // isDltEnabled ? null : (
1549
- // <CapButton
1550
- // type="flat"
1551
- // className="fallback-preview-btn"
1552
- // prefix={<CapIcon type="eye" />}
1553
- // style={{ color: CAP_SECONDARY.base }}
1554
- // onClick={() => setFallbackPreviewmode(true)}
1555
- // disabled={fallbackMessage === '' || fallbackMessageError}
1556
- // >
1557
- // {formatMessage(globalMessages.preview)}
1558
- // </CapButton>
1559
- // )
1560
- // }
1561
- // />
1562
- // </CapRow>,
1563
- // );
1564
-
1565
- //dlt is enabled, and dlt content is not yet added, show button to add dlt creative
1566
- // showAddCreativeBtnForDlt
1567
- // && contentArr.push(
1568
- // <CapCard className="rcs-dlt-fallback-card">
1569
- // <CapRow type="flex" justify="center" align="middle">
1570
- // <CapColumn span={10}>
1571
- // <CapImage src={addCreativesIcon} />
1572
- // </CapColumn>
1573
- // <CapColumn span={14}>
1574
- // <CapButton
1575
- // className="add-dlt-btn"
1576
- // type="secondary"
1577
- // onClick={addDltMsgHandler}
1578
- // >
1579
- // {formatMessage(messages.addSmsCreative)}
1580
- // </CapButton>
1581
- // </CapColumn>
1582
- // </CapRow>
1583
- // </CapCard>,
1584
- // );
1585
-
1586
- // //dlt is enabled and dlt content is added, show it in a card
1587
- // showCardForDlt
1588
- // && contentArr.push(
1589
- // <CapCustomCardList
1590
- // cardList={[getDltContentCardList(fallbackMessage, SMS)]}
1591
- // className="rcs-dlt-card"
1592
- // />,
1593
- // fallbackMessageError && (
1594
- // <CapError className="rcs-fallback-len-error">
1595
- // {formatMessage(messages.fallbackMsgLenError)}
1596
- // </CapError>
1597
- // ),
1598
- // );
1599
-
1600
- // //dlt is not enabled, show non dlt text area
1601
- // showNonDltFallbackComp
1602
- // && contentArr.push(
1603
- // <>
1604
- // <CapRow>
1605
- // <CapHeader
1606
- // title={(
1607
- // <CapHeading type="h4">
1608
- // {formatMessage(messages.fallbackTextAreaLabel)}
1609
- // </CapHeading>
1610
- // )}
1611
- // suffix={(
1612
- // <TagList
1613
- // label={formatMessage(globalMessages.addLabels)}
1614
- // onTagSelect={onTagSelectFallback}
1615
- // location={location}
1616
- // tags={tags || []}
1617
- // onContextChange={handleOnTagsContextChange}
1618
- // injectedTags={injectedTags || {}}
1619
- // selectedOfferDetails={selectedOfferDetails}
1620
- // />
1621
- // )}
1622
- // />
1623
- // </CapRow>
1624
- // <div className="rcs_fallback_msg_textarea_wrapper">
1625
- // <TextArea
1626
- // id="rcs_fallback_message_textarea"
1627
- // autosize={{ minRows: 3, maxRows: 5 }}
1628
- // placeholder={formatMessage(messages.fallbackMsgPlaceholder)}
1629
- // onChange={onFallbackMessageChange}
1630
- // errorMessage={fallbackMessageError}
1631
- // value={fallbackMessage || ""}
1632
- // />
1633
- // {!aiContentBotDisabled && (
1634
- // <CapAskAira.ContentGenerationBot
1635
- // text={fallbackMessage || ""}
1636
- // setText={(text) => {
1637
- // onFallbackMessageChange({ target: { value: text } });
1638
- // }}
1639
- // iconPlacement="float-br"
1640
- // rootStyle={{
1641
- // bottom: "0.5rem",
1642
- // right: "0.5rem",
1643
- // position: "absolute",
1644
- // }}
1645
- // />
1646
- // )}
1647
- // </div>
1648
- // <CapRow>{fallbackSmsLength()}</CapRow>
1649
- // </>
1650
- // );
1651
-
1652
- // return <>{contentArr}</>;
1653
- };
3031
+ const renderFallBackSmsComponent = () => (
3032
+ <SmsFallback
3033
+ value={smsFallbackData}
3034
+ onChange={setSmsFallbackData}
3035
+ parentLocation={location}
3036
+ smsRegister={smsRegister}
3037
+ isFullMode={isFullMode}
3038
+ selectedOfferDetails={selectedOfferDetails}
3039
+ channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
3040
+ sectionTitle={
3041
+ smsFallbackData
3042
+ ? formatMessage(messages.fallbackLabel)
3043
+ : formatMessage(messages.smsFallbackOptional)
3044
+ }
3045
+ templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
3046
+ templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
3047
+ /* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
3048
+ showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
3049
+ disableSelectTemplate={isEditFlow}
3050
+ eventContextTags={eventContextTags}
3051
+ onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
3052
+ isRcsEditFlow={isEditFlow}
3053
+ />
3054
+ );
1654
3055
 
1655
3056
  const uploadRcsImage = useCallback((file, type, fileParams, index) => {
1656
3057
  setImageError(null);
1657
- const isRcsThumbnail = index === 1;
3058
+ const isRcsThumbnail = isThumbnailAssetIndex(index);
1658
3059
  actions.uploadRcsAsset(file, type, {
1659
3060
  isRcsThumbnail,
1660
3061
  ...fileParams,
@@ -1692,10 +3093,7 @@ const splitTemplateVarString = (str) => {
1692
3093
  const updateOnRcsImageReUpload = useCallback(() => {
1693
3094
  setUpdateRcsImageSrc('');
1694
3095
  }, []);
1695
-
1696
-
1697
3096
  const uploadRcsVideo = (file, type, fileParams) => {
1698
- setImageError(null);
1699
3097
  actions.uploadRcsAsset(file, type, {
1700
3098
  ...fileParams,
1701
3099
  type: 'video',
@@ -1704,9 +3102,6 @@ const splitTemplateVarString = (str) => {
1704
3102
  });
1705
3103
  };
1706
3104
 
1707
- const updateRcsVideoSrc = (val) => {
1708
- setRcsVideoSrc(val);
1709
- };
1710
3105
  const setUpdateRcsVideoSrc = useCallback((index, val) => {
1711
3106
  setRcsVideoSrc(val);
1712
3107
  setAssetList(val);
@@ -1729,13 +3124,13 @@ const splitTemplateVarString = (str) => {
1729
3124
  updateRcsThumbnailSrc('');
1730
3125
  };
1731
3126
 
1732
- const renderThumbnailComponent = () => {
3127
+ const renderThumbnailComponent = () => {
1733
3128
  const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
1734
3129
  return !isEditFlow && (
1735
3130
  <>
1736
3131
  <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
1737
3132
  <CapImageUpload
1738
- style={{ paddingTop: '20px' }}
3133
+ className="rcs-image-upload-with-top-padding"
1739
3134
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1740
3135
  imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
1741
3136
  imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
@@ -1753,6 +3148,7 @@ const splitTemplateVarString = (str) => {
1753
3148
  channel={RCS}
1754
3149
  channelSpecificStyle={!isFullMode}
1755
3150
  skipDimensionValidation={true}
3151
+ showReUploadButton={!isEditFlow && isFullMode}
1756
3152
  />
1757
3153
  </>
1758
3154
  )
@@ -1778,7 +3174,7 @@ const splitTemplateVarString = (str) => {
1778
3174
  value: dim.type,
1779
3175
  label: `${dim.label}`
1780
3176
  }))}
1781
- style={{ marginBottom: '20px' }}
3177
+ className="rcs-dimension-select--bottom-spacing"
1782
3178
  />
1783
3179
  </>
1784
3180
  )}
@@ -1792,7 +3188,7 @@ const splitTemplateVarString = (str) => {
1792
3188
  </div>
1793
3189
  ) : (
1794
3190
  <CapImageUpload
1795
- style={{ paddingTop: '20px' }}
3191
+ className="rcs-image-upload-with-top-padding"
1796
3192
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1797
3193
  imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
1798
3194
  imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
@@ -1803,7 +3199,7 @@ const splitTemplateVarString = (str) => {
1803
3199
  updateImageSrc={setUpdateRcsImageSrc}
1804
3200
  updateOnReUpload={updateOnRcsImageReUpload}
1805
3201
  index={0}
1806
- className="cap-custom-image-upload"
3202
+ className="cap-custom-image-upload rcs-image-upload--top-spacing"
1807
3203
  key={`rcs-uploaded-image-${selectedDimension}`}
1808
3204
  imageData={rcsData}
1809
3205
  channel={RCS}
@@ -1814,7 +3210,7 @@ const splitTemplateVarString = (str) => {
1814
3210
 
1815
3211
  </>
1816
3212
  );
1817
- }
3213
+ }
1818
3214
 
1819
3215
  const renderVideoComponent = () => {
1820
3216
  const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
@@ -1833,7 +3229,7 @@ const splitTemplateVarString = (str) => {
1833
3229
  value: dim.type,
1834
3230
  label: `${dim.label}`
1835
3231
  }))}
1836
- style={{ marginBottom: '20px' }}
3232
+ className="rcs-dimension-select--bottom-spacing"
1837
3233
  />
1838
3234
  )}
1839
3235
  {(isEditFlow || !isFullMode) ? (
@@ -1891,10 +3287,48 @@ const splitTemplateVarString = (str) => {
1891
3287
  };
1892
3288
 
1893
3289
  const getRcsPreview = () => {
3290
+
3291
+ if (templateType === contentType.carousel) {
3292
+ const cardsForPreview = buildCarouselCardsForPreview(carouselData);
3293
+ const carouselDimKey = getCarouselDimensionKey();
3294
+ const carouselImgDims =
3295
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
3296
+ const carouselVidDims =
3297
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
3298
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
3299
+ // Debug log for embedded/library mode preview payload (carousel)
3300
+ // eslint-disable-next-line no-console
3301
+ return (
3302
+ <UnifiedPreview
3303
+ channel={RCS}
3304
+ content={{
3305
+ carouselData: cardsForPreview,
3306
+ carouselPreviewDimensions: {
3307
+ imageWidth: carouselImgDims.width,
3308
+ imageHeight: carouselImgDims.height,
3309
+ videoThumbWidth: carouselVidDims.width,
3310
+ videoThumbHeight: carouselVidDims.height,
3311
+ },
3312
+ }}
3313
+ device={ANDROID}
3314
+ showDeviceToggle={false}
3315
+ showHeader={false}
3316
+ formatMessage={formatMessage}
3317
+ />
3318
+ );
3319
+ }
1894
3320
 
1895
3321
  const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
1896
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1897
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
3322
+ const isSlotMappingMode = isEditFlow || !isFullMode;
3323
+ const titleVarCountForResolve = isMediaTypeText
3324
+ ? 0
3325
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
3326
+ const resolvedTitle = isMediaTypeText
3327
+ ? ''
3328
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
3329
+ const resolvedDesc = isSlotMappingMode
3330
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
3331
+ : templateDesc;
1898
3332
  return (
1899
3333
  <UnifiedPreview
1900
3334
  channel={RCS}
@@ -1917,51 +3351,108 @@ const splitTemplateVarString = (str) => {
1917
3351
  );
1918
3352
  };
1919
3353
 
1920
- const getUnmappedDesc = (str, mapping) => {
1921
- if (!str) return '';
1922
- if (!mapping || Object.keys(mapping).length === 0) return str;
1923
- let result = str;
1924
- const replacements = [];
1925
- Object.entries(mapping).forEach(([key, value]) => {
1926
- const raw = (value ?? '').toString();
1927
- if (!raw || raw?.trim?.() === '') return;
1928
- const braced = /^\{\{[\s\S]*\}\}$/.test(raw) ? raw : `{{${raw}}}`;
1929
- replacements.push({ key, needle: raw });
1930
- if (braced !== raw) replacements.push({ key, needle: braced });
1931
- });
1932
- const seen = new Set();
1933
- const uniq = replacements
1934
- .filter(({ key, needle }) => {
1935
- const id = `${key}::${needle}`;
1936
- if (seen.has(id)) return false;
1937
- seen.add(id);
1938
- return true;
1939
- })
1940
- .sort((a, b) => (b.needle.length - a.needle.length));
1941
-
1942
- uniq.forEach(({ key, needle }) => {
1943
- if (!needle) return;
1944
- const escaped = needle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1945
- const regex = new RegExp(escaped, 'g');
1946
- result = result.replace(regex, `{{${key}}}`);
1947
- });
1948
- return result;
1949
- };
1950
-
1951
3354
  const createPayload = () => {
1952
- const base = get(dltEditData, `versions.base`, {});
1953
- const {
1954
- template_id: templateId = '',
1955
- template_name = '',
1956
- 'sms-editor': template = '',
1957
- header: registeredSenderIds = [],
1958
- } = base;
1959
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1960
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
3355
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1961
3356
  const alignment = isMediaTypeImage
1962
3357
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
1963
3358
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
1964
3359
 
3360
+ const heightTypeForCardWidth = isMediaTypeText
3361
+ ? undefined
3362
+ : isMediaTypeImage
3363
+ ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
3364
+ : isMediaTypeVideo
3365
+ ? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
3366
+ : undefined;
3367
+ const cardWidthFromSelection =
3368
+ heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
3369
+
3370
+ /** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
3371
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
3372
+ const smsFallbackMerged = !isFullMode
3373
+ ? (() => {
3374
+ const local =
3375
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
3376
+ return {
3377
+ ...smsFromApiShape,
3378
+ ...local,
3379
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
3380
+ };
3381
+ })()
3382
+ : (smsFallbackData || {});
3383
+ const smsFallbackForPayload = (() => {
3384
+ if (isFullMode) {
3385
+ return hasMeaningfulSmsFallbackShape(smsFallbackData) ? smsFallbackData : null;
3386
+ }
3387
+ const mapped = {
3388
+ templateName:
3389
+ smsFallbackMerged.templateName
3390
+ || smsFallbackMerged.smsTemplateName
3391
+ || '',
3392
+ // Use `||` so empty `content` does not block campaign/API `message` (common in embedded flows).
3393
+ content:
3394
+ smsFallbackMerged.content
3395
+ || smsFallbackMerged.smsContent
3396
+ || smsFallbackMerged.smsTemplateContent
3397
+ || smsFallbackMerged.message
3398
+ || '',
3399
+ templateContent:
3400
+ pickFirstSmsFallbackTemplateString(smsFallbackMerged)
3401
+ || '',
3402
+ ...(typeof smsFallbackMerged.unicodeValidity === 'boolean'
3403
+ && { unicodeValidity: smsFallbackMerged.unicodeValidity }),
3404
+ ...(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
3405
+ && Object.keys(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
3406
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
3407
+ smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
3408
+ }),
3409
+ };
3410
+ return hasMeaningfulSmsFallbackShape(mapped) ? mapped : null;
3411
+ })();
3412
+
3413
+ const carouselCardContent = isCarouselType
3414
+ ? (carouselData || []).map((card = {}) => {
3415
+ const cardMediaType = card.mediaType;
3416
+ const isCardVideo = cardMediaType === RCS_MEDIA_TYPES.VIDEO;
3417
+ const isCardImage = cardMediaType === RCS_MEDIA_TYPES.IMAGE;
3418
+ const mediaUrl = isCardVideo
3419
+ ? (card.videoAsset?.videoSrc || '')
3420
+ : (card.imageSrc || '');
3421
+ const thumbnailUrl = isCardVideo
3422
+ ? (card.thumbnailSrc || card.videoAsset?.videoThumbnail || '')
3423
+ : '';
3424
+ const cardVarTokens = [
3425
+ ...((card.title || '').match(rcsVarRegex) ?? []),
3426
+ ...((card.description || '').match(rcsVarRegex) ?? []),
3427
+ ];
3428
+ const cardVarMappedForCard = {};
3429
+ if (isSlotMappingMode && cardVarTokens.length > 0) {
3430
+ cardVarTokens.forEach((token) => {
3431
+ const varName = getVarNameFromToken(token);
3432
+ if (!varName) return;
3433
+ cardVarMappedForCard[varName] = sanitizeCardVarMappedValue(cardVarMapped?.[varName] ?? '');
3434
+ });
3435
+ }
3436
+ return {
3437
+ title: card.title || '',
3438
+ description: card.description || '',
3439
+ mediaType: cardMediaType,
3440
+ ...((isCardImage || isCardVideo) && {
3441
+ media: {
3442
+ mediaUrl,
3443
+ thumbnailUrl,
3444
+ height: selectedCarouselHeight || MEDIUM,
3445
+ ...(isCardVideo && card.videoAsset?.videoName && { videoName: card.videoAsset.videoName }),
3446
+ },
3447
+ }),
3448
+ ...(Array.isArray(card.suggestions) && card.suggestions.length > 0 && {
3449
+ suggestions: card.suggestions,
3450
+ }),
3451
+ ...(Object.keys(cardVarMappedForCard).length > 0 && { cardVarMapped: cardVarMappedForCard }),
3452
+ };
3453
+ })
3454
+ : null;
3455
+
1965
3456
  const payload = {
1966
3457
  name: templateName,
1967
3458
  versions: {
@@ -1970,16 +3461,22 @@ const splitTemplateVarString = (str) => {
1970
3461
  RCS: {
1971
3462
  rcsContent: {
1972
3463
  ...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
1973
- cardType: STANDALONE,
1974
- cardSettings: {
1975
- cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
1976
- ...(alignment && { mediaAlignment: alignment }),
1977
- cardWidth: SMALL,
1978
- },
1979
- cardContent: [
3464
+ cardType: isCarouselType ? contentType.carousel : STANDALONE,
3465
+ cardSettings: isCarouselType
3466
+ ? { cardOrientation: VERTICAL, cardWidth: selectedCarouselWidth || SMALL }
3467
+ : {
3468
+ cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
3469
+ ...(alignment && { mediaAlignment: alignment }),
3470
+ cardWidth: SMALL,
3471
+ },
3472
+ cardContent: isCarouselType
3473
+ ? carouselCardContent
3474
+ : [
1980
3475
  {
1981
- title: resolvedTitle,
1982
- description: resolvedDesc,
3476
+ // Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
3477
+ // / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
3478
+ title: templateTitle,
3479
+ description: templateDesc,
1983
3480
  mediaType: templateMediaType,
1984
3481
  ...(!isMediaTypeText && {media: {
1985
3482
  mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
@@ -1988,23 +3485,32 @@ const splitTemplateVarString = (str) => {
1988
3485
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
1989
3486
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
1990
3487
  }}),
1991
- ...(!isFullMode && (() => {
1992
- const tokens = [
1993
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
1994
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
3488
+ ...(isSlotMappingMode && (() => {
3489
+ const templateVarTokens = [
3490
+ ...(templateTitle?.match(rcsVarRegex) ?? []),
3491
+ ...(templateDesc?.match(rcsVarRegex) ?? []),
1995
3492
  ];
1996
- const allowedKeys = tokens
1997
- .map((t) => getVarNameFromToken(t))
1998
- .filter(Boolean);
1999
- const nextMap = {};
2000
- allowedKeys.forEach((k) => {
2001
- if (Object.prototype.hasOwnProperty.call(cardVarMapped || {}, k)) {
2002
- nextMap[k] = cardVarMapped[k];
2003
- } else {
2004
- nextMap[k] = '';
2005
- }
3493
+ const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
3494
+ cardVarMapped,
3495
+ );
3496
+ // Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
3497
+ // semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
3498
+ // and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
3499
+ const persistedSlotVarMap = {};
3500
+ templateVarTokens.forEach((token, slotIndexZeroBased) => {
3501
+ const varName = getVarNameFromToken(token);
3502
+ if (!varName) return;
3503
+ const resolvedRawValue = resolveCardVarMappedSlotValue(
3504
+ cardVarMappedForRcsCardOnly,
3505
+ varName,
3506
+ slotIndexZeroBased,
3507
+ isSlotMappingMode,
3508
+ rcsSpanningSemanticVarNames.has(varName),
3509
+ );
3510
+ const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
3511
+ persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
2006
3512
  });
2007
- return { cardVarMapped: nextMap };
3513
+ return { cardVarMapped: persistedSlotVarMap };
2008
3514
  })()),
2009
3515
  ...(suggestions.length > 0 && { suggestions }),
2010
3516
  }
@@ -2012,17 +3518,109 @@ const splitTemplateVarString = (str) => {
2012
3518
  contentType: isFullMode ? templateType : RICHCARD,
2013
3519
  ...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
2014
3520
  },
2015
- smsFallBackContent: {
2016
- message: fallbackMessage,
2017
- ...(isDltEnabled && {
2018
- templateConfigs: {
2019
- templateId,
2020
- templateName: template_name,
2021
- template,
2022
- registeredSenderIds,
2023
- },
2024
- }),
2025
- },
3521
+ ...(smsFallbackForPayload && (() => {
3522
+ const smsBodyText =
3523
+ smsFallbackForPayload.content
3524
+ || smsFallbackForPayload.templateContent
3525
+ || smsFallbackForPayload.message
3526
+ || smsFallbackForPayload.smsContent
3527
+ || '';
3528
+ /**
3529
+ * Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
3530
+ * Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
3531
+ */
3532
+ const m = smsFallbackMerged || {};
3533
+ const tcSibling = m.templateConfigs && typeof m.templateConfigs === 'object'
3534
+ ? m.templateConfigs
3535
+ : {};
3536
+ const smsFallbackTemplateId =
3537
+ (m.smsTemplateId != null && String(m.smsTemplateId).trim() !== ''
3538
+ ? String(m.smsTemplateId)
3539
+ : '')
3540
+ || (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
3541
+ ? String(tcSibling.templateId)
3542
+ : '');
3543
+ const smsFallbackTemplateStr =
3544
+ pickFirstSmsFallbackTemplateString(m)
3545
+ || (typeof m.templateContent === 'string' ? m.templateContent : '')
3546
+ || (typeof tcSibling.template === 'string' ? tcSibling.template : '')
3547
+ || '';
3548
+ const smsFallbackTemplateName =
3549
+ m.templateName
3550
+ || m.smsTemplateName
3551
+ || tcSibling.templateName
3552
+ || tcSibling.name
3553
+ || '';
3554
+ const registeredSenderIdsForPayload = Array.isArray(m.registeredSenderIds)
3555
+ ? m.registeredSenderIds
3556
+ : Array.isArray(tcSibling.registeredSenderIds)
3557
+ ? tcSibling.registeredSenderIds
3558
+ : Array.isArray(tcSibling.header)
3559
+ ? tcSibling.header
3560
+ : null;
3561
+ const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
3562
+ const smsFallbackTemplateConfigs =
3563
+ smsFallbackTemplateId || hasRegisteredSenderIds
3564
+ ? {
3565
+ ...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
3566
+ ...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
3567
+ ...(smsFallbackTemplateName && {
3568
+ templateName: smsFallbackTemplateName,
3569
+ }),
3570
+ ...(hasRegisteredSenderIds && {
3571
+ registeredSenderIds: registeredSenderIdsForPayload,
3572
+ }),
3573
+ }
3574
+ : null;
3575
+ const isDltCampaign = !isFullMode && isTraiDLTEnable(isFullMode, smsRegister);
3576
+ return {
3577
+ smsFallBackContent: isFullMode
3578
+ ? {
3579
+ smsTemplateName: smsFallbackForPayload.templateName || '',
3580
+ smsContent: smsBodyText,
3581
+ // cap-campaigns-v2 `normalizeRcsMessageContentForApi` only serializes `message` (+ templateConfigs); without this key SMS fallback is dropped on send.
3582
+ message: smsBodyText,
3583
+ ...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
3584
+ unicodeValidity: smsFallbackForPayload.unicodeValidity,
3585
+ }),
3586
+ ...(smsFallbackForPayload.rcsSmsFallbackVarMapped
3587
+ && Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
3588
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
3589
+ }),
3590
+ ...(smsFallbackTemplateConfigs && {
3591
+ templateConfigs: smsFallbackTemplateConfigs,
3592
+ }),
3593
+ }
3594
+ : {
3595
+ // Round-trip storage: full shape so reopening the editor restores template
3596
+ // name, sender IDs, and var mappings. normalizeRcsMessageContentForApi
3597
+ // (called in CreativesContainer getCreativesData) strips this to
3598
+ // { message, templateConfigs } before the API call — the extra fields here
3599
+ // are only used for re-hydration, never sent to the API.
3600
+ message: smsBodyText,
3601
+ ...(smsFallbackForPayload.templateName && {
3602
+ smsTemplateName: smsFallbackForPayload.templateName,
3603
+ }),
3604
+ ...(smsFallbackForPayload.templateContent && {
3605
+ templateContent: smsFallbackForPayload.templateContent,
3606
+ }),
3607
+ ...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
3608
+ unicodeValidity: smsFallbackForPayload.unicodeValidity,
3609
+ }),
3610
+ ...(Array.isArray(registeredSenderIdsForPayload) && {
3611
+ registeredSenderIds: registeredSenderIdsForPayload,
3612
+ }),
3613
+ ...(smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
3614
+ && Object.keys(smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
3615
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
3616
+ smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
3617
+ }),
3618
+ ...(isDltCampaign && smsFallbackTemplateConfigs && {
3619
+ templateConfigs: smsFallbackTemplateConfigs,
3620
+ }),
3621
+ },
3622
+ };
3623
+ })()),
2026
3624
  },
2027
3625
  },
2028
3626
  },
@@ -2032,6 +3630,141 @@ const splitTemplateVarString = (str) => {
2032
3630
  return payload;
2033
3631
  };
2034
3632
 
3633
+ /** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
3634
+ const testPreviewFormData = useMemo(() => {
3635
+ const payload = createPayload();
3636
+ const rcs = payload?.versions?.base?.content?.RCS;
3637
+ if (!rcs) return null;
3638
+ // createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
3639
+ const accountIdForCreateMessageMeta =
3640
+ (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
3641
+ ? String(wecrmAccountId)
3642
+ : accountId;
3643
+ const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
3644
+ let rcsForTest = {
3645
+ ...rcs,
3646
+ rcsContent: {
3647
+ ...rcs.rcsContent,
3648
+ ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
3649
+ },
3650
+ };
3651
+ /** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
3652
+ const cardContent = rcsForTest.rcsContent?.cardContent;
3653
+ if (Array.isArray(cardContent) && cardContent[0]) {
3654
+ if (isCarouselType) {
3655
+ // Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
3656
+ // user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
3657
+ // - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
3658
+ // - prepareTagExtractionPayload can extract tag metadata from the card content.
3659
+ // Track cumulative slotOffset across cards: each card's title/description vars occupy
3660
+ // sequential global slot indices ({{1}},{{2}} in card 0; {{3}},{{4}} in card 1, …).
3661
+ // Without the offset, every card restarts at slotKey="1" and resolves against card 0's
3662
+ // mappings, causing tags from card 1+ to be missing or wrong.
3663
+ let carouselSlotOffset = 0;
3664
+ rcsForTest = {
3665
+ ...rcsForTest,
3666
+ rcsContent: {
3667
+ ...rcsForTest.rcsContent,
3668
+ cardContent: cardContent.map((card) => {
3669
+ const rawTitle = card.title || '';
3670
+ const rawDesc = card.description || '';
3671
+ const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
3672
+ const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
3673
+ const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
3674
+ const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
3675
+ carouselSlotOffset += titleVarCount + descVarCount;
3676
+ return { ...card, title: resolvedTitle, description: resolvedDesc };
3677
+ }),
3678
+ },
3679
+ };
3680
+ } else if (isSlotMappingModeForPreview) {
3681
+ // Standalone card: coalesce cardVarMapped with the template's slot names so the preview
3682
+ // API receives a correctly-keyed var map for non-carousel templates.
3683
+ const fullCardVarMapped = coalesceCardVarMappedToTemplate(
3684
+ pickRcsCardVarMappedEntries(cardVarMapped),
3685
+ templateTitle,
3686
+ templateDesc,
3687
+ rcsVarRegex,
3688
+ );
3689
+ rcsForTest = {
3690
+ ...rcsForTest,
3691
+ rcsContent: {
3692
+ ...rcsForTest.rcsContent,
3693
+ cardContent: [
3694
+ { ...cardContent[0], cardVarMapped: fullCardVarMapped },
3695
+ ...cardContent.slice(1),
3696
+ ],
3697
+ },
3698
+ };
3699
+ }
3700
+ }
3701
+ const out = {
3702
+ versions: {
3703
+ base: {
3704
+ content: {
3705
+ RCS: rcsForTest,
3706
+ },
3707
+ },
3708
+ },
3709
+ };
3710
+ // Use the smsFallbackTemplateConfigs already built by createPayload() — it uses smsFallbackMerged
3711
+ // which merges smsFromApiShape (API baseline with templateId, registeredSenderIds, templateName, etc.)
3712
+ // with local smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced
3713
+ // smsTemplateId in campaign mode (smsFallbackMerged = { ...smsFromApiShape, ...local } there).
3714
+ // This also keeps templateName (and all other fields) in sync with the approval payload automatically.
3715
+ const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
3716
+ if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
3717
+ out.templateConfigs = {
3718
+ ...smsFallbackTcFromPayload,
3719
+ traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
3720
+ };
3721
+ }
3722
+ return out;
3723
+ }, [
3724
+ templateName,
3725
+ templateTitle,
3726
+ templateDesc,
3727
+ templateMediaType,
3728
+ cardVarMapped,
3729
+ carouselData,
3730
+ suggestions,
3731
+ rcsImageSrc,
3732
+ rcsVideoSrc,
3733
+ rcsThumbnailSrc,
3734
+ selectedDimension,
3735
+ smsFallbackData,
3736
+ isFullMode,
3737
+ isEditFlow,
3738
+ templateType,
3739
+ accountId,
3740
+ wecrmAccountId,
3741
+ accessToken,
3742
+ accountName,
3743
+ hostName,
3744
+ smsRegister,
3745
+ ]);
3746
+
3747
+ /**
3748
+ * Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
3749
+ * with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
3750
+ * miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
3751
+ */
3752
+ const librarySmsFallbackMergedForValidation = useMemo(() => {
3753
+ if (isFullMode) {
3754
+ return smsFallbackData;
3755
+ }
3756
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
3757
+ const local =
3758
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
3759
+ return {
3760
+ ...smsFromApiShape,
3761
+ ...local,
3762
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
3763
+ };
3764
+ }, [isFullMode, templateData, smsFallbackData]);
3765
+
3766
+
3767
+
2035
3768
  const actionCallback = ({ errorMessage, resp }, isEdit) => {
2036
3769
  // eslint-disable-next-line no-undef
2037
3770
  const error = errorMessage?.message || errorMessage;
@@ -2061,6 +3794,9 @@ const splitTemplateVarString = (str) => {
2061
3794
  _id: params?.id,
2062
3795
  validity: true,
2063
3796
  type: RCS,
3797
+ // CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
3798
+ // the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
3799
+ closeSlideBoxAfterSubmit: !isFullMode,
2064
3800
  };
2065
3801
  getFormData(formDataParams);
2066
3802
  };
@@ -2074,6 +3810,7 @@ const splitTemplateVarString = (str) => {
2074
3810
  actionCallback({ resp, errorMessage });
2075
3811
  setSpin(false); // Always turn off spinner
2076
3812
  if (!errorMessage) {
3813
+ setTemplateStatus(RCS_STATUSES.pending);
2077
3814
  onCreateComplete();
2078
3815
  }
2079
3816
  });
@@ -2085,6 +3822,91 @@ const splitTemplateVarString = (str) => {
2085
3822
  }
2086
3823
  };
2087
3824
 
3825
+ /** When a fallback SMS row exists, require non-empty body (trimmed) and filled var slots (DLT). */
3826
+ const smsFallbackBlocksDone = () => {
3827
+ // Non-DLT library: user removed SMS fallback (local null) but template still carries fallback — block Done.
3828
+ if (
3829
+ !isFullMode
3830
+ && !isTraiDLTEnable(isFullMode, smsRegister)
3831
+ && smsFallbackData == null
3832
+ && hasMeaningfulSmsFallbackShape(
3833
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
3834
+ )
3835
+ ) {
3836
+ return true;
3837
+ }
3838
+ if (!smsFallbackData) return false;
3839
+ // Full-mode (Send for approval): SMS fallback is optional. Tag-slot mapping is a display/preview
3840
+ // concern, not a structural requirement for approval — the registered SMS template body stands on
3841
+ // its own. Never block the Send for approval button due to missing or unfilled fallback var slots.
3842
+ if (isFullMode) return false;
3843
+ const merged = librarySmsFallbackMergedForValidation;
3844
+ const templateText = pickFirstSmsFallbackTemplateString(merged);
3845
+ if (!templateText) {
3846
+ return true;
3847
+ }
3848
+ const rawVarMap =
3849
+ merged.rcsSmsFallbackVarMapped
3850
+ || merged['rcs-sms-fallback-var-mapped'];
3851
+ const varMap =
3852
+ rawVarMap != null && typeof rawVarMap === 'object' ? rawVarMap : {};
3853
+ return !areAllRcsSmsFallbackVarSlotsFilled(templateText, varMap);
3854
+ };
3855
+
3856
+ /**
3857
+ * Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
3858
+ * semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
3859
+ * / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
3860
+ */
3861
+ const isLibraryCampaignCardVarMappingIncomplete = () => {
3862
+ if (isFullMode) return false;
3863
+ const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
3864
+ rcsVarTestRegex.test(elem),
3865
+ );
3866
+ const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
3867
+ rcsVarTestRegex.test(elem),
3868
+ );
3869
+ const orderedVarNames = [
3870
+ ...titleTokens.map((t) => t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
3871
+ ...descTokens.map((t) => t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
3872
+ ];
3873
+ if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
3874
+ return true;
3875
+ }
3876
+ return orderedVarNames.some((name, globalIdx) => {
3877
+ const v = resolveCardVarMappedSlotValue(
3878
+ cardVarMapped,
3879
+ name,
3880
+ globalIdx,
3881
+ true,
3882
+ rcsSpanningSemanticVarNames.has(name),
3883
+ );
3884
+ const s = v == null ? '' : String(v);
3885
+ return s.trim() === '';
3886
+ });
3887
+ };
3888
+
3889
+ const isCarouselLibraryIncomplete = () => {
3890
+ if (!isCarouselType || isFullMode) return false;
3891
+ if ((carouselErrors || []).some((err) => err?.title || err?.description)) return true;
3892
+ // Block if any card's title or description has no content at all (no static text and no variable tokens).
3893
+ const hasEmptyField = (carouselData || []).some((card) =>
3894
+ ['title', 'description'].some((field) => !(card?.[field] || '').trim())
3895
+ );
3896
+ if (hasEmptyField) return true;
3897
+ const unfilledVar = (carouselData || []).some((card) =>
3898
+ ['title', 'description'].some((field) => {
3899
+ const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((t) => rcsVarTestRegex.test(t));
3900
+ return tokens.some((t) => {
3901
+ const name = t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
3902
+ const v = cardVarMapped?.[name];
3903
+ return v == null || String(v).trim() === '';
3904
+ });
3905
+ })
3906
+ );
3907
+ return unfilledVar;
3908
+ };
3909
+
2088
3910
  const isDisableDone = () => {
2089
3911
  if(isEditFlow){
2090
3912
  return false;
@@ -2095,46 +3917,26 @@ const splitTemplateVarString = (str) => {
2095
3917
  }
2096
3918
  }
2097
3919
 
2098
- if(!isFullMode){
2099
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2100
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2101
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2102
-
2103
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2104
- return true;
2105
- }
2106
-
2107
- const hasEmptyMapping =
2108
- cardVarMapped &&
2109
- Object.keys(cardVarMapped).length > 0 &&
2110
- Object.entries(cardVarMapped).some(([_, v]) => {
2111
- if (typeof v !== 'string') return !v; // null/undefined
2112
- return v.trim() === ''; // empty string
2113
- });
2114
-
2115
- if (hasEmptyMapping) {
2116
- return true;
2117
- }
3920
+ if (isCarouselLibraryIncomplete()) {
3921
+ return true;
3922
+ }
2118
3923
 
2119
- const anyMissing = allVars.some(name => {
2120
- const v = cardVarMapped?.[name];
2121
- if (typeof v !== 'string') return !v;
2122
- return v.trim() === '';
2123
- });
2124
- if (anyMissing) {
2125
- return true;
2126
- }
3924
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
3925
+ return true;
2127
3926
  }
2128
3927
 
2129
- if (isMediaTypeText && templateDesc.trim() === '') {
3928
+ if (smsFallbackBlocksDone()) {
2130
3929
  return true;
3930
+ }
2131
3931
 
3932
+ if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
3933
+ return true;
2132
3934
  }
2133
- if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
3935
+ if (!isCarouselType && isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
2134
3936
  return true;
2135
3937
  }
2136
3938
 
2137
- if (isMediaTypeVideo && (rcsVideoSrc.videoSrc === '' || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
3939
+ if (!isCarouselType && isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
2138
3940
  return true;
2139
3941
  }
2140
3942
  if (buttonType.includes(CTA)) {
@@ -2146,72 +3948,60 @@ const splitTemplateVarString = (str) => {
2146
3948
  return true;
2147
3949
  }
2148
3950
  }
2149
- if (templateDescError || templateTitleError || fallbackMessageError) {
3951
+ if (templateDescError || templateTitleError) {
3952
+ return true;
3953
+ }
3954
+ if (
3955
+ smsFallbackData?.content
3956
+ && smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
3957
+ ) {
2150
3958
  return true;
2151
3959
  }
2152
3960
  return false;
2153
3961
  };
2154
3962
 
2155
3963
  const isEditDisableDone = () => {
2156
-
2157
- if (templateStatus !== RCS_STATUSES.approved) {
3964
+ if (isFullMode && !isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
2158
3965
  return true;
2159
3966
  }
2160
3967
 
2161
- if (!isFullMode) {
2162
- if (templateName.trim() === '' || templateNameError) {
2163
- return true;
2164
- }
3968
+ if (isCarouselLibraryIncomplete()) {
3969
+ return true;
2165
3970
  }
2166
- if(!isFullMode){
2167
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2168
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2169
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2170
-
2171
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2172
- return true;
2173
- }
2174
3971
 
2175
- const hasEmptyMapping =
2176
- cardVarMapped &&
2177
- Object.keys(cardVarMapped).length > 0 &&
2178
- Object.entries(cardVarMapped).some(([_, v]) => {
2179
- if (typeof v !== 'string') return !v; // null/undefined
2180
- return v.trim() === ''; // empty string
2181
- });
2182
-
2183
- if (hasEmptyMapping) {
2184
- return true;
2185
- }
3972
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
3973
+ return true;
3974
+ }
2186
3975
 
2187
- const anyMissing = allVars.some(name => {
2188
- const v = cardVarMapped?.[name];
2189
- if (typeof v !== 'string') return !v;
2190
- return v.trim() === '';
2191
- });
2192
- if (anyMissing) {
2193
- return true;
2194
- }
3976
+ if (smsFallbackBlocksDone()) {
3977
+ return true;
2195
3978
  }
2196
- if (isMediaTypeText && templateDesc.trim() === '') {
3979
+
3980
+ if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
2197
3981
  return true;
2198
3982
  }
2199
- if (isMediaTypeImage && rcsImageSrc === '') {
3983
+ if (!isCarouselType && isMediaTypeImage && rcsImageSrc === '') {
2200
3984
  return true;
2201
3985
  }
2202
- if(isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
3986
+ if (!isCarouselType && isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
2203
3987
  return true;
2204
3988
  }
2205
3989
 
2206
3990
  if (buttonType.includes(CTA)) {
2207
- const hasValidButtons = suggestions.every(suggestion =>
3991
+ const hasValidButtons = suggestions.every(suggestion =>
2208
3992
  suggestion.text && suggestion.url && !suggestionError && !forbiddenCharactersValidation(suggestion.text)
2209
3993
  );
2210
3994
  if (!hasValidButtons) {
2211
3995
  return true;
2212
3996
  }
2213
3997
  }
2214
- if (templateTitleError || templateDescError || fallbackMessageError) {
3998
+ if (templateTitleError || templateDescError) {
3999
+ return true;
4000
+ }
4001
+ if (
4002
+ smsFallbackData?.content
4003
+ && smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
4004
+ ) {
2215
4005
  return true;
2216
4006
  }
2217
4007
  return false;
@@ -2261,56 +4051,60 @@ const splitTemplateVarString = (str) => {
2261
4051
  };
2262
4052
 
2263
4053
  const getMainContent = () => {
2264
- if (showDltContainer && !fallbackPreviewmode) {
2265
- const dltSlideBoxContent = showDltContainer && getDltSlideBoxContent();
2266
- const { dltHeader = '', dltContent = '' } = dltSlideBoxContent;
2267
- return (
2268
- <CapSlideBox
2269
- show={showDltContainer}
2270
- header={dltHeader}
2271
- content={dltContent}
2272
- handleClose={closeDltContainerHandler}
2273
- size="size-xl"
2274
- />
2275
- );
2276
- }
2277
-
4054
+ // Slideboxes are rendered outside the page-level spinner to avoid
4055
+ // stacking/blur issues during initial loads.
4056
+ if (showDltContainer) return null;
2278
4057
  return (
2279
4058
  <>
2280
- {templateStatus !== '' && (<CapRow useLegacy className="template-status-container">
2281
- <CapLabel type="label2">
2282
- {formatMessage(messages.templateStatusLabel)}
2283
- </CapLabel>
2284
-
2285
- {templateStatus && (
2286
- <CapAlert
2287
- message={getTemplateStatusMessage()}
2288
- type={getTemplateStatusType(templateStatus)}
2289
- />
2290
- )}
2291
- </CapRow>
4059
+ {templateStatus !== '' && (
4060
+ <CapRow useLegacy className="template-status-container">
4061
+ <CapColumn span={14}>
4062
+ <CapLabel type="label2">
4063
+ {formatMessage(messages.templateStatusLabel)}
4064
+ </CapLabel>
4065
+
4066
+ {!isHostInfoBip && templateStatus && (
4067
+ <CapAlert
4068
+ message={getTemplateStatusMessage()}
4069
+ type={getTemplateStatusType(templateStatus)}
4070
+ />
4071
+ )}
4072
+ </CapColumn>
4073
+ </CapRow>
2292
4074
  )}
2293
- <CapRow className="cap-rcs-creatives">
4075
+ <CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
2294
4076
  <CapColumn span={14}>
2295
4077
  {/* template name */}
2296
4078
  {isFullMode && (
2297
- <CapInput
2298
- id="rcs_template_name_input"
2299
- data-testid="template_name"
2300
- onChange={onTemplateNameChange}
2301
- errorMessage={templateNameError}
2302
- placeholder={formatMessage(
2303
- globalMessages.templateNamePlaceholder,
2304
- )}
2305
- value={templateName || ''}
2306
- size="default"
2307
- label={formatMessage(globalMessages.creativeNameLabel)}
2308
- disabled={(isEditFlow || !isFullMode)}
2309
- />
4079
+ isEditFlow ? (
4080
+ <div className="rcs-creative-name-readonly">
4081
+ <CapHeading type="h4">
4082
+ {formatMessage(globalMessages.creativeNameLabel)}
4083
+ </CapHeading>
4084
+ <CapHeading type="h5" className="rcs-creative-name-value">
4085
+ {templateName || '-'}
4086
+ </CapHeading>
4087
+ </div>
4088
+ ) : (
4089
+ <CapInput
4090
+ id="rcs_template_name_input"
4091
+ data-testid="template_name"
4092
+ onChange={onTemplateNameChange}
4093
+ errorMessage={templateNameError}
4094
+ placeholder={formatMessage(
4095
+ globalMessages.templateNamePlaceholder,
4096
+ )}
4097
+ value={templateName || ''}
4098
+ size="default"
4099
+ label={formatMessage(globalMessages.creativeNameLabel)}
4100
+ disabled={(isEditFlow || !isFullMode)}
4101
+ />
4102
+ )
2310
4103
  )}
2311
4104
  {renderLabel('templateTypeLabel')}
2312
4105
  <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
2313
- <CapRadioGroup
4106
+
4107
+ <CapRadioGroup
2314
4108
  id="select-rcs-template-type"
2315
4109
  options={TEMPLATE_TYPE_OPTIONS}
2316
4110
  onChange={onTemplateTypeChange}
@@ -2319,27 +4113,37 @@ const splitTemplateVarString = (str) => {
2319
4113
  />
2320
4114
  </ConfigProvider>
2321
4115
 
2322
- {/* Show media only for rich_card or carousel */}
2323
- {(templateType === contentType.rich_card || templateType === contentType.carousel) && (
4116
+ {templateType === contentType.carousel ? (
4117
+ renderCarouselSection()
4118
+ ) : (
2324
4119
  <>
2325
- {renderLabel('mediaLabel')}
2326
- <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
4120
+ {/* Show media only for rich_card */}
4121
+ {templateType === contentType.rich_card && (
4122
+ <>
4123
+ {renderLabel('mediaLabel')}
4124
+ <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
2327
4125
  <CapRadioGroup
2328
- options={mediaRadioOptions || []}
2329
- value={templateMediaType}
2330
- onChange={onTemplateMediaTypeChange}
2331
- disabled={(isEditFlow || !isFullMode)}
2332
- className="rcs-radio"
2333
- />
2334
- </ConfigProvider>
4126
+ options={mediaRadioOptions || []}
4127
+ value={templateMediaType}
4128
+ onChange={onTemplateMediaTypeChange}
4129
+ disabled={(isEditFlow || !isFullMode)}
4130
+ className="rcs-radio"
4131
+ />
4132
+ </ConfigProvider>
2335
4133
  <div className="rcs-container-image">
2336
- {getMediaBasedComponent()}
2337
- </div>
4134
+ {getMediaBasedComponent()}
4135
+ </div>
4136
+ </>
4137
+ )}
4138
+ {renderTextComponent()}
4139
+ </>
4140
+ )}
4141
+ {(!isEditFlow || hasMeaningfulSmsFallbackShape(smsFallbackData)) && (
4142
+ <>
4143
+ <CapDivider className="rcs-fallback-section-divider" />
4144
+ {renderFallBackSmsComponent()}
2338
4145
  </>
2339
4146
  )}
2340
- {renderTextComponent()}
2341
- <CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
2342
- {renderFallBackSmsComponent()}
2343
4147
  <div className="rcs-scroll-div" />
2344
4148
  </CapColumn>
2345
4149
  <CapColumn span={8} offset={2}>
@@ -2350,7 +4154,8 @@ const splitTemplateVarString = (str) => {
2350
4154
 
2351
4155
 
2352
4156
  <div className="rcs-footer">
2353
- {!isEditFlow && (
4157
+ {/* Full-mode create only: send-for-approval + disabled test/preview. Library mode uses Done below — onDoneCallback() is undefined when !isFullMode, so do not render this row (avoids a no-op primary button). */}
4158
+ {!isEditFlow && isFullMode && (
2354
4159
  <>
2355
4160
  <div className="button-disabled-tooltip-wrapper">
2356
4161
  <CapButton
@@ -2358,7 +4163,9 @@ const splitTemplateVarString = (str) => {
2358
4163
  disabled={isDisableDone()}
2359
4164
  className="rcs-done-btn"
2360
4165
  >
2361
- <FormattedMessage {...messages.sendForApprovalButtonLabel} />
4166
+ <FormattedMessage
4167
+ {...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
4168
+ />
2362
4169
  </CapButton>
2363
4170
  </div>
2364
4171
  <CapTooltip
@@ -2371,7 +4178,6 @@ const splitTemplateVarString = (str) => {
2371
4178
  className="rcs-test-preview-btn"
2372
4179
  type="secondary"
2373
4180
  disabled={true}
2374
- style={{ marginLeft: "8px" }}
2375
4181
  >
2376
4182
  <FormattedMessage {...creativesMessages.testAndPreview} />
2377
4183
  </CapButton>
@@ -2392,7 +4198,7 @@ const splitTemplateVarString = (str) => {
2392
4198
  </div>
2393
4199
  </>
2394
4200
  )}
2395
- {isEditFlow && templateStatus === RCS_STATUSES.approved && (
4201
+ {isEditFlow && (isHostInfoBip || templateStatus === RCS_STATUSES.approved) && (
2396
4202
  <>
2397
4203
  <CapButton
2398
4204
  onClick={handleTestAndPreview}
@@ -2404,51 +4210,6 @@ const splitTemplateVarString = (str) => {
2404
4210
  </>
2405
4211
  )}
2406
4212
  </div>
2407
-
2408
-
2409
- {fallbackPreviewmode && (
2410
- <CapSlideBox
2411
- className="rcs-fallback-preview"
2412
- show={fallbackPreviewmode}
2413
- header={(
2414
- <CapHeading type="h7" style={{ color: CAP_G01 }}>
2415
- {formatMessage(messages.fallbackPreviewtitle)}
2416
- </CapHeading>
2417
- )}
2418
- content={(
2419
- <>
2420
- <UnifiedPreview
2421
- channel={RCS}
2422
- content={{
2423
- rcsPreviewContent: {
2424
- rcsDesc: tempMsg,
2425
- },
2426
- }}
2427
- device={ANDROID}
2428
- showDeviceToggle={false}
2429
- showHeader={false}
2430
- formatMessage={formatMessage}
2431
- />
2432
- <CapHeading
2433
- type="h3"
2434
- style={{ textAlign: 'center', width: '100%' }}
2435
- className="margin-t-16"
2436
- >
2437
- {formatMessage(messages.totalCharacters, {
2438
- smsCount: Math.ceil(
2439
- tempMsg?.length / FALLBACK_MESSAGE_MAX_LENGTH,
2440
- ),
2441
- number: tempMsg.length,
2442
- })}
2443
- </CapHeading>
2444
- </>
2445
- )}
2446
- handleClose={() => {
2447
- setFallbackPreviewmode(false);
2448
- setDltPreviewData('');
2449
- }}
2450
- />
2451
- )}
2452
4213
  </>
2453
4214
  );
2454
4215
  };
@@ -2457,23 +4218,57 @@ const splitTemplateVarString = (str) => {
2457
4218
  <CapSpin spinning={loadingTags || spin}>
2458
4219
  {getMainContent()}
2459
4220
  </CapSpin>
4221
+
4222
+ {showDltContainer && (() => {
4223
+ const { dltHeader = '', dltContent = '' } = getDltSlideBoxContent() || {};
4224
+ return (
4225
+ <CapSlideBox
4226
+ show={showDltContainer}
4227
+ header={dltHeader}
4228
+ content={dltContent}
4229
+ handleClose={closeDltContainerHandler}
4230
+ size="size-xl"
4231
+ />
4232
+ );
4233
+ })()}
4234
+
2460
4235
  <TestAndPreviewSlidebox
2461
4236
  show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
2462
4237
  onClose={handleCloseTestAndPreview}
2463
- formData={null} // RCS doesn't use formData structure like SMS
2464
- content={getTemplateContent()}
4238
+ formData={testPreviewFormData}
4239
+ content={testAndPreviewContent}
2465
4240
  currentChannel={RCS}
4241
+ orgUnitId={orgUnitId}
4242
+ rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
4243
+ smsFallbackContent={
4244
+ smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
4245
+ ? {
4246
+ templateContent:
4247
+ smsFallbackData.templateContent || smsFallbackData.content || '',
4248
+ templateName: smsFallbackData.templateName || '',
4249
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
4250
+ ? mergeRcsSmsFallbackVarMapLayers(
4251
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
4252
+ smsFallbackData,
4253
+ )
4254
+ : mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
4255
+ }
4256
+ : null
4257
+ }
4258
+ smsRegister={smsRegister}
2466
4259
  />
2467
4260
  </>
2468
4261
  );
2469
4262
  };
2470
4263
 
4264
+
2471
4265
  const mapStateToProps = createStructuredSelector({
2472
4266
  rcsData: makeSelectRcs(),
2473
4267
  accountData: makeSelectAccount(),
2474
4268
  metaEntities: makeSelectMetaEntities(),
2475
4269
  loadingTags: isLoadingMetaEntities(),
2476
4270
  injectedTags: setInjectedTags(),
4271
+ currentOrgDetails: selectCurrentOrgDetails(),
2477
4272
  });
2478
4273
 
2479
4274
  const mapDispatchToProps = (dispatch) => ({