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

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