@capillarytech/creatives-library 9.0.29 → 9.0.30

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 (147) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +261 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  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 +108 -15
  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 +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +322 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  96. package/v2Containers/Rcs/carouselUtils.js +224 -0
  97. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  98. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  99. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  100. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  101. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  102. package/v2Containers/Rcs/constants.js +132 -16
  103. package/v2Containers/Rcs/index.js +1822 -946
  104. package/v2Containers/Rcs/index.scss +443 -8
  105. package/v2Containers/Rcs/messages.js +45 -22
  106. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +494 -0
  107. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  108. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  109. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  110. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  111. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  112. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74440 -39387
  113. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  114. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  115. package/v2Containers/Rcs/tests/index.test.js +275 -40
  116. package/v2Containers/Rcs/tests/mockData.js +38 -0
  117. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +912 -0
  118. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  119. package/v2Containers/Rcs/utils.js +514 -12
  120. package/v2Containers/Sms/Create/index.js +115 -48
  121. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  122. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  123. package/v2Containers/SmsTrai/Create/index.js +9 -4
  124. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  125. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  126. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  127. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  128. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  129. package/v2Containers/SmsWrapper/index.js +37 -8
  130. package/v2Containers/TagList/index.js +5 -0
  131. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  132. package/v2Containers/Templates/_templates.scss +196 -12
  133. package/v2Containers/Templates/actions.js +11 -0
  134. package/v2Containers/Templates/constants.js +2 -0
  135. package/v2Containers/Templates/index.js +131 -59
  136. package/v2Containers/Templates/sagas.js +57 -13
  137. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  138. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  139. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  140. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  141. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  142. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  143. package/v2Containers/TemplatesV2/index.js +88 -25
  144. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  145. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  146. package/v2Containers/Whatsapp/index.js +3 -20
  147. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -1,13 +1,12 @@
1
1
  /* eslint-disable no-unused-expressions */
2
- import React, { useState, useEffect, useCallback } from 'react';
2
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
3
3
  import { bindActionCreators } from 'redux';
4
4
  import { createStructuredSelector } from 'reselect';
5
- import { injectIntl, FormattedMessage } from 'react-intl';
5
+ import { FormattedMessage } from 'react-intl';
6
6
  import get from 'lodash/get';
7
7
  import isEmpty from 'lodash/isEmpty';
8
8
  import cloneDeep from 'lodash/cloneDeep';
9
9
  import isNil from 'lodash/isNil';
10
- import styled from 'styled-components';
11
10
  import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
12
11
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
13
12
  import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
@@ -23,8 +22,6 @@ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
23
22
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
24
23
  import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
25
24
  import CapImage from '@capillarytech/cap-ui-library/CapImage';
26
- import CapCard from '@capillarytech/cap-ui-library/CapCard';
27
- import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
28
25
  import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
29
26
  import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
30
27
  import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
@@ -35,6 +32,8 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
35
32
  import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
36
33
  import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
37
34
  import CapLink from '@capillarytech/cap-ui-library/CapLink';
35
+ import CapTab from '@capillarytech/cap-ui-library/CapTab';
36
+ import { flushSync } from 'react-dom';
38
37
 
39
38
  import {
40
39
  CAP_G01,
@@ -50,17 +49,29 @@ import {
50
49
  import CapVideoUpload from '../../v2Components/CapVideoUpload';
51
50
  import * as globalActions from '../Cap/actions';
52
51
  import CapActionButton from '../../v2Components/CapActionButton';
52
+ import TemplatePreview from '../../v2Components/TemplatePreview';
53
53
  import { makeSelectRcs, makeSelectAccount } from './selectors';
54
54
  import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
55
55
  import {
56
56
  isLoadingMetaEntities,
57
57
  makeSelectMetaEntities,
58
58
  setInjectedTags,
59
+ selectCurrentOrgDetails,
59
60
  } from '../Cap/selectors';
60
61
  import * as RcsActions from './actions';
61
62
  import { isAiContentBotDisabled } from '../../utils/common';
62
- import * as TemplatesActions from '../Templates/actions';
63
63
  import './index.scss';
64
+ import {
65
+ normalizeLibraryLoadedTitleDesc,
66
+ mergeRcsSmsFallbackVarMapLayers,
67
+ applySmsFallbackHydration,
68
+ syncCardVarMappedSemanticsFromSlots,
69
+ hasMeaningfulSmsFallbackShape,
70
+ getLibrarySmsFallbackApiBaselineFromTemplateData,
71
+ mergeSmsFallbackForLibrary,
72
+ buildSmsFallBackContentForPayload,
73
+ pickRcsCardVarMappedEntries,
74
+ } from './rcsLibraryHydrationUtils';
64
75
  import {
65
76
  RCS,
66
77
  SMS,
@@ -75,8 +86,8 @@ import {
75
86
  LIBRARY,
76
87
  ALLOWED_IMAGE_EXTENSIONS_REGEX,
77
88
  RCS_IMG_SIZE,
78
- RCS_DLT_MODE,
79
89
  CTA,
90
+ AI_CONTENT_BOT_DISABLED,
80
91
  RCS_STATUSES,
81
92
  TITLE_TEXT,
82
93
  MESSAGE_TEXT,
@@ -91,9 +102,13 @@ import {
91
102
  rcsVarTestRegex,
92
103
  RCS_IMAGE_DIMENSIONS,
93
104
  RCS_TEXT_MESSAGE_MAX_LENGTH,
105
+ RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
94
106
  RCS_RICH_CARD_MAX_LENGTH,
95
107
  RCS_VIDEO_THUMBNAIL_DIMENSIONS,
108
+ RCS_CAROUSEL_IMAGE_DIMENSIONS,
109
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
96
110
  MAX_BUTTONS,
111
+ INITIAL_SUGGESTIONS,
97
112
  INITIAL_SUGGESTIONS_DATA_STOP,
98
113
  RCS_BUTTON_TYPES,
99
114
  titletype,
@@ -103,25 +118,66 @@ import {
103
118
  SMALL,
104
119
  MEDIUM,
105
120
  RICHCARD,
121
+ HOST_INFOBIP,
122
+ HOST_ICS,
123
+ STOP,
124
+ RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
125
+ MAX_RCS_CAROUSEL_ALLOWED,
126
+ RCS_CAROUSEL_INITIAL_CARD,
127
+ RCS_CAROUSEL_INITIAL_FIRST_CARD,
128
+ RCS_NUMERIC_VAR_NAME_REGEX,
129
+ RCS_NUMERIC_VAR_TOKEN_REGEX,
130
+ RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
131
+ RCS_TAG_AREA_FIELD_TITLE,
132
+ RCS_TAG_AREA_FIELD_DESC,
133
+ REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
134
+ JS_TYPES,
106
135
  } from './constants';
107
136
  import globalMessages from '../Cap/messages';
108
137
  import messages from './messages';
109
138
  import creativesMessages from '../CreativesContainer/messages';
110
139
  import withCreatives from '../../hoc/withCreatives';
111
140
  import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
112
- 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';
113
143
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
144
+ import { splitTemplateVarString } from '../../utils/templateVarUtils';
114
145
  import CapImageUpload from '../../v2Components/CapImageUpload';
115
- import addCreativesIcon from '../Assets/images/addCreativesIllustration.svg';
116
- import Templates from '../Templates';
117
- import SmsTraiEdit from '../SmsTrai/Edit';
146
+ import SmsFallback from '../../v2Components/SmsFallback';
147
+ import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
118
148
  import TagList from '../TagList';
119
149
  import { validateTags } from '../../utils/tagValidations';
120
- import { getCdnUrl } from '../../utils/cdnTransformation';
150
+ import { isTraiDLTEnable } from '../../utils/common';
121
151
  import { isTagIncluded } from '../../utils/commonUtils';
122
152
  import injectReducer from '../../utils/injectReducer';
123
153
  import v2RcsReducer from './reducer';
124
- import { getTemplateStatusType } from './utils';
154
+ import {
155
+ buildRcsNumericMustachePlaceholderRegex,
156
+ getTemplateStatusType,
157
+ normalizeCardVarMapped,
158
+ coalesceCardVarMappedToTemplate,
159
+ getRcsSemanticVarNamesSpanningTitleAndDesc,
160
+ resolveCardVarMappedSlotValue,
161
+ sanitizeCardVarMappedValue,
162
+ checkSmsFallbackBlocksDone,
163
+ isSmsFallbackContentTooLong,
164
+ } from './utils';
165
+ import {
166
+ getCarouselImageAssetIndex,
167
+ getCarouselVideoAssetIndex,
168
+ getCarouselThumbnailAssetIndex,
169
+ isThumbnailAssetIndex,
170
+ getCarouselDimensionKey,
171
+ ensureFirstCardDefaultPhoneSuggestions,
172
+ checkDisableAddCarouselButton,
173
+ getNextCarouselVarToken,
174
+ getCarouselTitleCharacterCount,
175
+ getCarouselDescriptionCharacterCount,
176
+ buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
177
+ buildCarouselCardContentForPayload,
178
+ } from './carouselUtils';
179
+ import CarouselDimensionSelection from './components/CarouselDimensionSelection';
180
+ import CarouselCard from './components/CarouselCard';
125
181
 
126
182
 
127
183
  const { Group: CapCheckboxGroup } = CapCheckbox;
@@ -135,31 +191,48 @@ export const Rcs = (props) => {
135
191
  templateData = {},
136
192
  rcsData = {},
137
193
  thumbnailData = {}, // Add separate thumbnail data
138
- templatesActions,
139
194
  globalActions,
140
195
  location,
141
- handleClose,
142
196
  getDefaultTags,
143
197
  supportedTags,
144
198
  metaEntities,
145
199
  injectedTags,
146
200
  loadingTags,
147
201
  getFormData,
148
- isDltEnabled,
149
202
  smsRegister,
203
+ orgUnitId,
150
204
  selectedOfferDetails,
151
205
  eventContextTags,
152
- waitEventContextTags,
153
206
  accountData = {},
207
+ currentOrgDetails,
154
208
  // TestAndPreviewSlidebox props
155
209
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
156
210
  handleTestAndPreview: propsHandleTestAndPreview,
157
211
  handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
212
+ handleClose,
158
213
  } = props || {};
159
214
  const { formatMessage } = intl;
160
215
  const { TextArea } = CapInput;
161
216
  const { CapCustomCardList } = CapCustomCard;
217
+
218
+ // Defensive: React cannot render plain objects as children (crashes with
219
+ // "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
220
+ // can accidentally set an error state to an object (e.g. `{}`).
221
+ const normalizeErrorMessage = (err) => {
222
+ if (!err) return '';
223
+ if (React.isValidElement(err)) return err;
224
+ if (typeof err === JS_TYPES.STRING) return err;
225
+ if (typeof err === JS_TYPES.NUMBER) return String(err);
226
+ if (typeof err === JS_TYPES.OBJECT && typeof err.message === JS_TYPES.STRING) return err.message;
227
+ try {
228
+ return JSON.stringify(err);
229
+ } catch (e) {
230
+ return String(err);
231
+ }
232
+ };
233
+
162
234
  const [isEditFlow, setEditFlow] = useState(false);
235
+ const isEditLike = isEditFlow || !isFullMode;
163
236
  const [tags, updateTags] = useState([]);
164
237
  const [spin, setSpin] = useState(false);
165
238
  //template
@@ -168,33 +241,17 @@ export const Rcs = (props) => {
168
241
  const [templateMediaType, setTemplateMediaType] = useState(
169
242
  RCS_MEDIA_TYPES.NONE,
170
243
  );
171
- const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
172
244
  const [templateTitle, setTemplateTitle] = useState('');
173
245
  const [templateDesc, setTemplateDesc] = useState('');
174
246
  const [templateDescError, setTemplateDescError] = useState(false);
175
247
  const [templateStatus, setTemplateStatus] = useState('');
176
- const [templateDate, setTemplateDate] = useState('');
177
- //fallback
178
- const [fallbackMessage, setFallbackMessage] = useState('');
179
- const [fallbackMessageError, setFallbackMessageError] = useState(false);
180
- //fallback dlt
181
- const [showDltContainer, setShowDltContainer] = useState(false);
182
- const [dltMode, setDltMode] = useState('');
183
- const [dltEditData, setDltEditData] = useState({});
184
- const [showDltCard, setShowDltCard] = useState(false);
185
- const [fallbackPreviewmode, setFallbackPreviewmode] = useState(false);
186
- const [dltPreviewData, setDltPreviewData] = useState('');
248
+ /** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
249
+ const [smsFallbackData, setSmsFallbackData] = useState(undefined);
187
250
  const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
188
- const [buttonType, setButtonType] = useState(RCS_BUTTON_TYPES.NONE);
251
+ const buttonType = RCS_BUTTON_TYPES.NONE;
189
252
  const [suggestionError, setSuggestionError] = useState(true);
253
+ const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
190
254
  const [templateType, setTemplateType] = useState('text_message');
191
- const [templateHeader, setTemplateHeader] = useState('');
192
- const [templateMessage, setTemplateMessage] = useState('');
193
- const [templateHeaderError, setTemplateHeaderError] = useState('');
194
- const [templateMessageError, setTemplateMessageError] = useState('');
195
- const validVarRegex = /\{\{(\d+)\}\}/g;
196
- const [updatedTitleData, setUpdatedTitleData] = useState([]);
197
- const [updatedDescData, setUpdatedDescData] = useState([]);
198
255
  const [titleVarMappedData, setTitleVarMappedData] = useState({});
199
256
  const [descVarMappedData, setDescVarMappedData] = useState({});
200
257
  const [titleTextAreaId, setTitleTextAreaId] = useState();
@@ -205,75 +262,58 @@ export const Rcs = (props) => {
205
262
  const [rcsVideoSrc, setRcsVideoSrc] = useState({});
206
263
  const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
207
264
  const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
208
- const [imageError, setImageError] = useState(null);
265
+ // Carousel (UI-only) state
266
+ const [selectedCarousel, setSelectedCarousel] = useState('');
267
+ const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
268
+ const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
269
+ const [carouselData, setCarouselData] = useState([]);
270
+ const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
271
+ const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
272
+ const [carouselResetNonce, setCarouselResetNonce] = useState(0);
273
+ const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
274
+ const [imageError, setImageError] = useState(null);
209
275
  const [templateTitleError, setTemplateTitleError] = useState(false);
210
276
  const [cardVarMapped, setCardVarMapped] = useState({});
277
+ /** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
278
+ const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
279
+ const lastHydratedRcsCardVarSignatureRef = useRef(null);
280
+
281
+ /**
282
+ * Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
283
+ * without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
284
+ * fallback card / content stopped appearing until re-selected.
285
+ */
286
+ const rcsHydrationDetails = useMemo(
287
+ () => (isFullMode ? rcsData?.templateDetails : templateData),
288
+ [isFullMode, rcsData?.templateDetails, templateData],
289
+ );
211
290
 
212
- // TestAndPreviewSlidebox state
213
- const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
214
- const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
215
-
216
- const tempMsg = dltPreviewData === '' ? fallbackMessage : dltPreviewData;
217
-
218
- // Get template content for TestAndPreviewSlidebox
219
- // Reference: Based on getRcsPreview() function (lines 2087-2111) which prepares content for TemplatePreview
220
- // getRcsPreview ALWAYS uses templateTitle and templateDesc for ALL template types (text_message, rich_card, carousel)
221
- // renderTextComponent (lines 1317-1485) also uses templateTitle and templateDesc
222
- // Note: templateHeader and templateMessage are defined but NOT used in the component
223
- const getTemplateContent = useCallback(() => {
224
- const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
225
- const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
226
- const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
227
-
228
- // Build media preview object (same pattern as getRcsPreview)
229
- const mediaPreview = {};
230
- if (isMediaTypeImage && rcsImageSrc) {
231
- mediaPreview.rcsImageSrc = rcsImageSrc;
232
- }
233
- if (isMediaTypeVideo && !isMediaTypeText) {
234
- // For video, use thumbnailSrc as rcsVideoSrc (same as getRcsPreview line 2104)
235
- if (rcsThumbnailSrc) {
236
- mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
237
- } else if (rcsVideoSrc?.videoSrc) {
238
- mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
239
- }
240
- // Also include thumbnailSrc separately if available
241
- if (rcsThumbnailSrc) {
242
- mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
291
+ /** Skip duplicate /meta/TAG fetches: same query is triggered from (1) useEffect below, (2) title TagList mount, (3) description TagList mount — each calls getTagsforContext('Outbound'). */
292
+ const lastTagSchemaQueryKeyRef = useRef(null);
293
+ /**
294
+ * Library: parent often passes a new `templateData` object reference every render. Re-applying the same
295
+ * SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
296
+ */
297
+ const lastSmsFallbackHydrationKeyRef = useRef(null);
298
+
299
+ const fetchTagSchemaIfNewQuery = useCallback(
300
+ (query) => {
301
+ const key = JSON.stringify(query);
302
+ if (lastTagSchemaQueryKeyRef.current === key) {
303
+ return;
243
304
  }
244
- }
245
-
246
- // Build content object
247
- // Reference: getRcsPreview (line 2091-2092) uses templateTitle and templateDesc for ALL cases
248
- // templateTitle is used for rich_card/carousel title, empty for text_message
249
- // templateDesc is used for ALL types (text message body or rich card description)
250
- // For UnifiedPreview, we map templateTitle -> templateHeader and templateDesc -> templateMessage
251
- const contentObj = {
252
- // Map templateTitle to templateHeader and templateDesc to templateMessage
253
- templateHeader: templateTitle,
254
- templateMessage: templateDesc,
255
- ...mediaPreview,
256
- ...(suggestions.length > 0 && {
257
- suggestions: suggestions,
258
- }),
259
- };
305
+ lastTagSchemaQueryKeyRef.current = key;
306
+ globalActions.fetchSchemaForEntity(query);
307
+ },
308
+ [globalActions],
309
+ );
260
310
 
261
- return contentObj;
262
- }, [
263
- templateMediaType,
264
- templateTitle,
265
- templateDesc,
266
- rcsImageSrc,
267
- rcsVideoSrc,
268
- rcsThumbnailSrc,
269
- suggestions,
270
- selectedDimension,
271
- ]);
311
+ // TestAndPreviewSlidebox state
312
+ const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
272
313
 
273
314
  // Handle Test and Preview button click
274
315
  const handleTestAndPreview = useCallback(() => {
275
316
  setShowTestAndPreviewSlidebox(true);
276
- setIsTestAndPreviewMode(true);
277
317
  if (propsHandleTestAndPreview) {
278
318
  propsHandleTestAndPreview();
279
319
  }
@@ -297,31 +337,332 @@ export const Rcs = (props) => {
297
337
  // For video
298
338
  return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
299
339
  };
340
+ /** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
341
+ const handleSmsFallbackEditorStateChange = useCallback((patch) => {
342
+ setSmsFallbackData((prev) => {
343
+ if (!patch || typeof patch !== 'object') return prev;
344
+ // Bail out when no template has been selected yet — SmsTraiEdit fires
345
+ // onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
346
+ // which would create a non-null smsFallbackData from nothing and cause
347
+ // the card to appear as "Untitled creative" before any save.
348
+ if (!prev) return prev;
349
+ return { ...prev, ...patch };
350
+ });
351
+ }, []);
300
352
 
353
+ /** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
301
354
  const [accountId, setAccountId] = useState('');
355
+ /** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
356
+ const [wecrmAccountId, setWecrmAccountId] = useState('');
302
357
  const [accessToken, setAccessToken] = useState('');
303
358
  const [hostName, setHostName] = useState('');
304
359
  const [accountName, setAccountName] = useState('');
360
+ const isHostInfoBip = hostName === HOST_INFOBIP;
361
+ const isHostIcs = hostName === HOST_ICS;
362
+
363
+ useEffect(() => {
364
+ setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
365
+ }, [isHostIcs]);
305
366
  const [rcsAccount, setRcsAccount] = useState('');
306
367
  useEffect(() => {
307
368
  const accountObj = accountData.selectedRcsAccount || {};
308
369
  if (!isEmpty(accountObj)) {
309
370
  const {
371
+ id: wecrmId,
310
372
  sourceAccountIdentifier = '',
311
373
  configs = {},
312
374
  } = accountObj;
313
-
314
375
  setAccountId(sourceAccountIdentifier);
376
+ setWecrmAccountId(
377
+ wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
378
+ );
315
379
  setAccessToken(configs.accessToken || '');
316
380
  setHostName(accountObj.hostName || '');
317
381
  setAccountName(accountObj.name || '');
318
382
  setRcsAccount(accountObj.id || '');
383
+ } else {
384
+ setAccountId('');
385
+ setWecrmAccountId('');
386
+ setAccessToken('');
387
+ setHostName('');
388
+ setAccountName('');
319
389
  }
320
390
  }, [accountData.selectedRcsAccount]);
321
391
 
322
392
  const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
323
393
  const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
324
394
  const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
395
+ const isCarouselType = templateType === contentType.carousel;
396
+
397
+ const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
398
+ setCarouselData((prev = []) => {
399
+ const updated = cloneDeep(prev);
400
+ if (!updated?.[cardIndex]) return updated;
401
+ if (clearImage) updated[cardIndex].imageSrc = '';
402
+ if (clearVideo) updated[cardIndex].videoAsset = {};
403
+ if (clearThumb) updated[cardIndex].thumbnailSrc = '';
404
+ return updated;
405
+ });
406
+
407
+ if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
408
+ if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
409
+ if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
410
+ };
411
+
412
+ const resetCarouselMediaForAllCards = () => {
413
+ setCarouselData((prev = []) => {
414
+ const updated = cloneDeep(prev);
415
+ updated.forEach((card) => {
416
+ if (!card) return;
417
+ card.imageSrc = '';
418
+ card.videoAsset = {};
419
+ card.thumbnailSrc = '';
420
+ });
421
+ return updated;
422
+ });
423
+ (carouselData || []).forEach((_, idx) => {
424
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
425
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
426
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
427
+ });
428
+ // Force tab panes to remount after global reset (some tab implementations cache inactive panes)
429
+ setCarouselResetNonce((count) => count + 1);
430
+ };
431
+
432
+ // Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
433
+ // updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
434
+ // suggestions typed since the last commit (stale closure).
435
+ const handleCarouselValueChange = (carouselIndex, fields) => {
436
+ setCarouselData((prev = []) => {
437
+ const updated = cloneDeep(prev);
438
+ if (!updated[carouselIndex]) return prev;
439
+ fields.forEach(({ fieldName, value }) => {
440
+ updated[carouselIndex][fieldName] = value;
441
+ });
442
+ return updated;
443
+ });
444
+ };
445
+
446
+ const updateCarouselErrors = (carouselIndex, patch) => {
447
+ setCarouselErrors((prev = []) => {
448
+ const next = Array.isArray(prev) ? [...prev] : [];
449
+ next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
450
+ return next;
451
+ });
452
+ };
453
+
454
+ const deleteCarouselCard = (index) => {
455
+ let nextIdx = 0;
456
+ flushSync(() => {
457
+ setCarouselData((prev = []) => {
458
+ const updated = cloneDeep(prev);
459
+ if (index < 0 || index >= updated.length) return updated;
460
+ updated.splice(index, 1);
461
+ nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
462
+ return ensureFirstCardDefaultPhoneSuggestions(updated);
463
+ });
464
+ });
465
+ setCarouselErrors((prev = []) => {
466
+ const next = Array.isArray(prev) ? [...prev] : [];
467
+ next.splice(index, 1);
468
+ return next;
469
+ });
470
+ setActiveCarouselIndex(`${nextIdx}`);
471
+ };
472
+
473
+
474
+ const addCarouselCard = () => {
475
+ let newIndex = 0;
476
+ flushSync(() => {
477
+ setCarouselData((prev = []) => {
478
+ const updated = cloneDeep(prev);
479
+ updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
480
+ newIndex = updated.length - 1;
481
+ return updated;
482
+ });
483
+ });
484
+ setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
485
+ setActiveCarouselIndex(`${newIndex}`);
486
+ };
487
+
488
+ // Initialize carousel data when switching to carousel type
489
+ useEffect(() => {
490
+ if (!isCarouselType) return;
491
+ if (!carouselData || carouselData.length === 0) {
492
+ setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
493
+ setCarouselErrors([{}]);
494
+ setActiveCarouselIndex('0');
495
+ }
496
+ }, [isCarouselType]);
497
+
498
+ // keep derived carousel key in sync
499
+ useEffect(() => {
500
+ if (!isCarouselType) return;
501
+ if (!selectedCarouselHeight || !selectedCarouselWidth) return;
502
+ // Required format: HEIGHT_WIDTH
503
+ setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
504
+ }, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
505
+
506
+ const renderCarouselDimensionSelection = () => {
507
+ if (!isCarouselType) return null;
508
+ return (
509
+ <CarouselDimensionSelection
510
+ isEditFlow={isEditFlow}
511
+ isFullMode={isFullMode}
512
+ selectedCarouselHeight={selectedCarouselHeight}
513
+ selectedCarouselWidth={selectedCarouselWidth}
514
+ selectedCarousel={selectedCarousel}
515
+ onHeightChange={(val) => {
516
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
517
+ resetCarouselMediaForAllCards();
518
+ setSelectedCarouselHeight(val);
519
+ }}
520
+ onWidthChange={(val) => {
521
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
522
+ resetCarouselMediaForAllCards();
523
+ setSelectedCarouselWidth(val);
524
+ }}
525
+ />
526
+ );
527
+ };
528
+
529
+ const getCarouselTabPanes = () => {
530
+ const tagsForPicker = getRcsTags();
531
+ return (carouselData || []).map((card, index) => ({
532
+ key: String(index),
533
+ tab: index + 1,
534
+ content: (
535
+ <CarouselCard
536
+ cardIndex={index}
537
+ card={card}
538
+ carouselLength={carouselData.length}
539
+ isEditFlow={isEditFlow}
540
+ isFullMode={isFullMode}
541
+ mediaRadioOptions={mediaRadioOptions}
542
+ cardError={carouselErrors?.[index]}
543
+ location={location}
544
+ tagsForPicker={tagsForPicker}
545
+ injectedTags={injectedTags}
546
+ selectedOfferDetails={selectedOfferDetails}
547
+ formatMessage={formatMessage}
548
+ titleMaxLength={getTitleMaxLength()}
549
+ descriptionMaxLength={getDescriptionMaxLength()}
550
+ titleCharCount={getCarouselTitleCharacterCount(carouselData, index)}
551
+ descriptionCharCount={getCarouselDescriptionCharacterCount(carouselData, index)}
552
+ dimKey={getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth)}
553
+ rcsData={rcsData}
554
+ uploadRcsImage={uploadRcsImage}
555
+ uploadRcsVideo={uploadRcsVideo}
556
+ hostName={hostName}
557
+ onDelete={() => deleteCarouselCard(index)}
558
+ onMediaTypeChange={(value) => {
559
+ // Reset media fields when switching type
560
+ if (value === RCS_MEDIA_TYPES.IMAGE) {
561
+ // Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
562
+ clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
563
+ handleCarouselValueChange(index, [
564
+ { fieldName: 'mediaType', value },
565
+ { fieldName: 'videoAsset', value: {} },
566
+ { fieldName: 'thumbnailSrc', value: '' },
567
+ ]);
568
+ } else {
569
+ // Switching to VIDEO: clear image upload so it doesn't auto-restore.
570
+ clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
571
+ handleCarouselValueChange(index, [
572
+ { fieldName: 'mediaType', value },
573
+ { fieldName: 'imageSrc', value: '' },
574
+ ]);
575
+ }
576
+ }}
577
+ onTitleChange={({ target: { value } }) => {
578
+ let error = false;
579
+ if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
580
+ error = formatMessage(messages.templateHeaderLengthError);
581
+ } else {
582
+ error = variableErrorHandling(value);
583
+ }
584
+ updateCarouselErrors(index, { title: error });
585
+ handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
586
+ }}
587
+ onDescriptionChange={({ target: { value } }) => {
588
+ let error = false;
589
+ if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
590
+ error = formatMessage(messages.templateMessageLengthError);
591
+ } else {
592
+ error = variableErrorHandling(value);
593
+ }
594
+ updateCarouselErrors(index, { description: error });
595
+ handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
596
+ }}
597
+ onTitleAddVar={() => appendVarToCarouselField(index, 'title')}
598
+ onDescriptionAddVar={() => appendVarToCarouselField(index, 'description')}
599
+ onTagSelect={onCarouselTagSelect}
600
+ onContextChange={handleOnTagsContextChange}
601
+ renderEditMessage={renderCarouselEditMessage}
602
+ onFieldChange={(fieldName, value) => handleCarouselValueChange(index, [{ fieldName, value }])}
603
+ onSuggestionsChange={(nextSuggestions) =>
604
+ handleCarouselValueChange(index, [{ fieldName: 'suggestions', value: nextSuggestions }])}
605
+ />
606
+ ),
607
+ }));
608
+ };
609
+
610
+ const renderCarouselSection = () => {
611
+ if (!isCarouselType) return null;
612
+
613
+ const isMaxCarouselCardsReached = MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0);
614
+ const addCardTooltipTitle = isMaxCarouselCardsReached
615
+ ? formatMessage(messages.rcsCarouselMaxCardTooltip, { max: MAX_RCS_CAROUSEL_ALLOWED })
616
+ // "Set up the first card" only makes sense while creating a new template —
617
+ // in edit flow the cards already exist, so don't show it there.
618
+ : (!isEditFlow && checkDisableAddCarouselButton(carouselData, activeCarouselIndex) ? formatMessage(messages.rcsCarouselAddCardDisabledTooltip) : '');
619
+
620
+ // No "+" in edit flow (cards can't be added there) — and with no "+", the
621
+ // divider before it has nothing to separate, so skip both together instead
622
+ // of rendering a dangling divider.
623
+ const operations = isEditFlow ? null : (
624
+ <>
625
+ <CapDivider type="vertical" />
626
+ <CapTooltip title={addCardTooltipTitle}>
627
+ <span className="button-disabled-tooltip-wrapper">
628
+ <CapButton
629
+ onClick={addCarouselCard}
630
+ type="flat"
631
+ className="add-carousel-content-button"
632
+ disabled={
633
+ isEditFlow ||
634
+ !isFullMode ||
635
+ isMaxCarouselCardsReached ||
636
+ checkDisableAddCarouselButton(carouselData, activeCarouselIndex)
637
+ }
638
+ >
639
+ <CapIcon type="plus" />
640
+ </CapButton>
641
+ </span>
642
+ </CapTooltip>
643
+ </>
644
+ );
645
+
646
+ return (
647
+ <CapRow className={`rcs-carousel-section ${isEditFlow ? 'rcs-carousel-section--readonly-above' : ''}`}>
648
+ {renderCarouselDimensionSelection()}
649
+ <CapRow className="rcs-carousel-tab">
650
+ <CapTab
651
+ key={`rcs-carousel-tab-${carouselResetNonce}`}
652
+ defaultActiveKey="0"
653
+ activeKey={activeCarouselIndex}
654
+ tabBarExtraContent={operations}
655
+ onChange={(key) => {
656
+ setActiveCarouselIndex(`${key}`);
657
+ // reset focused var when switching cards (as per requirement)
658
+ setCarouselFocusedVarId('');
659
+ }}
660
+ panes={getCarouselTabPanes()}
661
+ />
662
+ </CapRow>
663
+ </CapRow>
664
+ );
665
+ };
325
666
 
326
667
  const mediaRadioOptions = [
327
668
  {
@@ -330,11 +671,16 @@ export const Rcs = (props) => {
330
671
  },
331
672
  {
332
673
  value: RCS_MEDIA_TYPES.VIDEO,
333
- label: formatMessage(messages.mediaVideo),
674
+ label: formatMessage(
675
+ templateType === contentType.carousel
676
+ ? messages.carouselMediaVideoOption
677
+ : messages.mediaVideo,
678
+ ),
334
679
  },
335
680
  ];
336
681
  const aiContentBotDisabled = isAiContentBotDisabled();
337
682
 
683
+
338
684
  const updateButtonChange = (data, index) => {
339
685
  if (data && data.text) {
340
686
  const forbiddenError = forbiddenCharactersValidation(data.text);
@@ -371,7 +717,9 @@ export const Rcs = (props) => {
371
717
  if (isFullMode) return;
372
718
  if (loadingTags || !tags || tags.length === 0) return;
373
719
  const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
374
- const resolved = resolveTemplateWithMap(templateStr); // placeholders -> mapped value (or '')
720
+ const slotOffset =
721
+ type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
722
+ const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
375
723
  if (!resolved) {
376
724
  if (type === TITLE_TEXT) setTemplateTitleError(false);
377
725
  if (type === MESSAGE_TEXT) setTemplateDescError(false);
@@ -380,8 +728,8 @@ export const Rcs = (props) => {
380
728
 
381
729
  let contentForValidation = resolved;
382
730
  const placeholderTokens = templateStr.match(rcsVarRegex) || [];
383
- placeholderTokens.forEach((t) => {
384
- const escaped = t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
731
+ placeholderTokens.forEach((token) => {
732
+ const escaped = token.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
385
733
  contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
386
734
  });
387
735
  if (!contentForValidation.trim()) {
@@ -398,10 +746,16 @@ export const Rcs = (props) => {
398
746
  tagModule: getDefaultTags,
399
747
  isFullMode,
400
748
  }) || {};
401
- const errorMsg =
402
- (validationResponse?.isBraceError &&
403
- formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
404
- false;
749
+ const unsupportedTagsLengthCheck =
750
+ validationResponse?.unsupportedTags?.length > 0;
751
+ const errorMsg =
752
+ (unsupportedTagsLengthCheck &&
753
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
754
+ unsupportedTags: validationResponse.unsupportedTags,
755
+ })) ||
756
+ (validationResponse.isBraceError &&
757
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
758
+ false;
405
759
  if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
406
760
  if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
407
761
  };
@@ -414,55 +768,214 @@ export const Rcs = (props) => {
414
768
  validateResolvedTagsForType(MESSAGE_TEXT);
415
769
  }, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
416
770
 
771
+ useEffect(() => {
772
+ if (isFullMode || !isCarouselType) return;
773
+ if (loadingTags || !tags || tags.length === 0) return;
774
+ (carouselData || []).forEach((card, idx) => {
775
+ ['title', 'description'].forEach((field) => {
776
+ const templateStr = card?.[field] || '';
777
+ if (!templateStr) return;
778
+ const resolved = resolveTemplateWithMap(templateStr);
779
+ if (!resolved) {
780
+ updateCarouselErrors(idx, { [field]: false });
781
+ return;
782
+ }
783
+ let contentForValidation = resolved;
784
+ const placeholderTokens = templateStr.match(rcsVarRegex) || [];
785
+ placeholderTokens.forEach((token) => {
786
+ const escaped = token.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
787
+ contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
788
+ });
789
+ if (!contentForValidation.trim()) {
790
+ updateCarouselErrors(idx, { [field]: false });
791
+ return;
792
+ }
793
+ const validationResponse = validateTags({
794
+ content: contentForValidation,
795
+ tagsParam: tags,
796
+ injectedTagsParams: injectedTags,
797
+ location,
798
+ tagModule: getDefaultTags,
799
+ eventContextTags,
800
+ isFullMode,
801
+ }) || {};
802
+ const errorMsg =
803
+ (validationResponse?.unsupportedTags?.length > 0 &&
804
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
805
+ unsupportedTags: validationResponse.unsupportedTags,
806
+ })) ||
807
+ (validationResponse.isBraceError &&
808
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
809
+ false;
810
+ updateCarouselErrors(idx, { [field]: errorMsg });
811
+ });
812
+ });
813
+ }, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
814
+
417
815
  const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
418
816
 
419
- const resolveTemplateWithMap = (str = '') => {
817
+ const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
818
+
819
+ /** Same `{{tag}}` in both title and description must not share one semantic map entry. */
820
+ const rcsSpanningSemanticVarNames = useMemo(
821
+ () => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
822
+ [templateTitle, templateDesc],
823
+ );
824
+
825
+ /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
826
+ const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
827
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
828
+ const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
829
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
830
+ let varOrdinal = 0;
831
+ for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
832
+ const segmentToken = templateSegments[segmentIndexInField];
833
+ if (rcsVarTestRegex.test(segmentToken)) {
834
+ if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
835
+ return offset + varOrdinal;
836
+ }
837
+ varOrdinal += 1;
838
+ }
839
+ }
840
+ return null;
841
+ };
842
+
843
+ /**
844
+ * Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
845
+ * multi-field variable resolution.
846
+ */
847
+ const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
420
848
  if (!str) return '';
421
- const arr = splitTemplateVarString(str);
849
+ const arr = splitTemplateVarStringRcs(str);
850
+ let varOrdinal = 0;
422
851
  return arr.map((elem) => {
423
852
  if (rcsVarTestRegex.test(elem)) {
424
853
  const key = getVarNameFromToken(elem);
425
- const v = cardVarMapped?.[key];
426
- if (isNil(v) || String(v)?.trim?.() === '') return elem;
427
- return String(v);
854
+ const globalSlot = slotOffset + varOrdinal;
855
+ varOrdinal += 1;
856
+ const slotValue = resolveCardVarMappedSlotValue(
857
+ cardVarMapped,
858
+ key,
859
+ globalSlot,
860
+ isEditLike,
861
+ rcsSpanningSemanticVarNames.has(key),
862
+ );
863
+ if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
864
+ return String(slotValue);
428
865
  }
429
866
  return elem;
430
867
  }).join('');
431
868
  };
432
869
 
870
+ const buildCarouselCardsForPreview = (cards = []) =>
871
+ buildCarouselCardsForPreviewUtil(cards, { isFullMode, rcsVarRegex, resolveTemplateWithMap });
433
872
 
434
- useEffect(() => {
435
- if (isFullMode || isEditFlow) return;
436
- const tokens = [
437
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
438
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
439
- ];
440
- if (!tokens.length) return;
441
- setCardVarMapped((prev) => {
442
- const next = { ...(prev || {}) };
443
- let changed = false;
444
- tokens.forEach((t) => {
445
- const name = getVarNameFromToken(t);
446
- if (name && !Object.prototype.hasOwnProperty.call(next, name)) {
447
- next[name] = '';
448
- changed = true;
449
- }
450
- });
451
- return changed ? next : prev;
452
- });
453
- }, [isFullMode, templateTitle, templateDesc]);
873
+ /**
874
+ * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
875
+ * (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
876
+ * TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
877
+ */
878
+ const getTemplateContent = useCallback(() => {
879
+ if (templateType === contentType.carousel) {
880
+ const carouselDimKey = getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth);
881
+ const carouselImgDims =
882
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
883
+ const carouselVidDims =
884
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
885
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
886
+ return {
887
+ carouselData: buildCarouselCardsForPreview(carouselData),
888
+ carouselPreviewDimensions: {
889
+ imageWidth: carouselImgDims.width,
890
+ imageHeight: carouselImgDims.height,
891
+ videoThumbWidth: carouselVidDims.width,
892
+ videoThumbHeight: carouselVidDims.height,
893
+ },
894
+ };
895
+ }
896
+ const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
897
+ const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
898
+ const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
454
899
 
900
+ const isSlotMappingMode = isEditFlow || !isFullMode;
901
+ const titleVarCountForResolve = isMediaTypeText
902
+ ? 0
903
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
904
+ const resolvedTitle = isMediaTypeText
905
+ ? ''
906
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
907
+ const resolvedDesc = isSlotMappingMode
908
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
909
+ : templateDesc;
910
+
911
+ const mediaPreview = {};
912
+ if (isMediaTypeImage && rcsImageSrc) {
913
+ mediaPreview.rcsImageSrc = rcsImageSrc;
914
+ }
915
+ if (isMediaTypeVideo && !isMediaTypeText) {
916
+ if (rcsThumbnailSrc) {
917
+ mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
918
+ } else if (rcsVideoSrc?.videoSrc) {
919
+ mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
920
+ }
921
+ if (rcsThumbnailSrc) {
922
+ mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
923
+ }
924
+ }
925
+
926
+ const contentObj = {
927
+ templateHeader: resolvedTitle,
928
+ templateMessage: resolvedDesc,
929
+ ...mediaPreview,
930
+ ...(suggestions.length > 0 && {
931
+ suggestions: suggestions,
932
+ }),
933
+ };
934
+
935
+ return contentObj;
936
+ }, [
937
+ templateMediaType,
938
+ templateTitle,
939
+ templateDesc,
940
+ rcsImageSrc,
941
+ rcsVideoSrc,
942
+ rcsThumbnailSrc,
943
+ suggestions,
944
+ selectedDimension,
945
+ isFullMode,
946
+ isEditFlow,
947
+ cardVarMapped,
948
+ rcsSpanningSemanticVarNames,
949
+ carouselData,
950
+ selectedCarouselHeight,
951
+ selectedCarouselWidth,
952
+ ]);
953
+
954
+ const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
455
955
 
456
- const RcsLabel = styled.div`
457
- display: flex;
458
- margin-top: 20px;
459
- `;
460
956
  const paramObj = params || {};
461
957
  useEffect(() => {
462
958
  const { id } = paramObj;
463
959
  if (id && isFullMode) {
464
960
  setSpin(true);
465
961
  actions.getTemplateDetails(id, setSpin);
962
+ } else if (!id && isFullMode) {
963
+ // Create New: clear standalone media and ALL possible carousel card Redux slots.
964
+ // Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
965
+ updateRcsImageSrc('');
966
+ setRcsVideoSrc({});
967
+ setRcsThumbnailSrc('');
968
+ setAssetList({});
969
+ setEditFlow(false);
970
+ actions.clearRcsMediaAsset(0);
971
+ actions.clearRcsMediaAsset(1);
972
+ for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
973
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
974
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
975
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
976
+ }
977
+ setCarouselData([]);
978
+ setCarouselErrors([]);
466
979
  }
467
980
  return () => {
468
981
  actions.clearEditResponse();
@@ -470,59 +983,68 @@ export const Rcs = (props) => {
470
983
  }, [paramObj.id]);
471
984
 
472
985
  useEffect(() => {
473
- if (!(isEditFlow || !isFullMode)) return;
986
+ if (!(isEditFlow || !isFullMode)) return;
474
987
 
475
- const initField = (targetString, currentVarMap, setVarMap, setUpdated) => {
476
- const arr = splitTemplateVarString(targetString);
477
- if (!arr?.length) {
478
- setVarMap({});
479
- setUpdated([]);
480
- return;
988
+ const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
989
+
990
+ const initField = (targetString, setVarMap, slotOffset) => {
991
+ const arr = splitTemplateVarStringRcs(targetString);
992
+ if (!arr?.length) {
993
+ setVarMap({});
994
+ return;
995
+ }
996
+ const nextVarMap = {};
997
+ let varOrdinal = 0;
998
+ arr.forEach((elem, idx) => {
999
+ // Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
1000
+ if (rcsVarTestRegex.test(elem)) {
1001
+ const id = `${elem}_${idx}`;
1002
+ const varName = getVarNameFromToken(elem);
1003
+ const globalSlot = slotOffset + varOrdinal;
1004
+ varOrdinal += 1;
1005
+ const mappedValue = resolveCardVarMappedSlotValue(
1006
+ cardVarMapped,
1007
+ varName,
1008
+ globalSlot,
1009
+ isEditLike,
1010
+ rcsSpanningSemanticVarNames.has(varName),
1011
+ );
1012
+ nextVarMap[id] = mappedValue;
481
1013
  }
482
- const nextVarMap = {};
483
- const nextUpdated = [...arr];
484
- arr.forEach((elem, idx) => {
485
- // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
486
- if (rcsVarTestRegex.test(elem)) {
487
- const id = `${elem}_${idx}`;
488
- const varName = getVarNameFromToken(elem);
489
- const mappedValue = (cardVarMapped?.[varName] ?? '').toString();
490
- nextVarMap[id] = mappedValue;
491
- if (mappedValue !== '') {
492
- nextUpdated[idx] = mappedValue;
493
- } else {
494
- nextUpdated[idx] = elem;
495
- }
496
- }
497
- });
498
- setVarMap(nextVarMap);
499
- setUpdated(nextUpdated);
500
- };
1014
+ });
1015
+ setVarMap(nextVarMap);
1016
+ };
501
1017
 
502
- initField(templateTitle, titleVarMappedData, setTitleVarMappedData, setUpdatedTitleData);
503
- initField(templateDesc, descVarMappedData, setDescVarMappedData, setUpdatedDescData);
504
- }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
505
-
506
- useEffect(() => {
507
- if(!isEditFlow && isFullMode){
1018
+ initField(templateTitle, setTitleVarMappedData, 0);
1019
+ initField(templateDesc, setDescVarMappedData, titleTokenCount);
1020
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
1021
+
1022
+ useEffect(() => {
1023
+ if (!isEditFlow && isFullMode) {
508
1024
  setRcsVideoSrc({});
509
1025
  updateRcsImageSrc('');
510
- setUpdateRcsImageSrc('');
511
- updateRcsThumbnailSrc('');
1026
+ setRcsThumbnailSrc('');
512
1027
  setAssetList({});
513
- }
514
- }, [templateMediaType]);
1028
+ }
1029
+ }, [templateMediaType]);
515
1030
 
516
- const templateStatusHelper = (details) => {
517
- const status = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
518
- switch (status) {
1031
+ /** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
1032
+ const templateStatusHelper = (cardContentFirst) => {
1033
+ const raw =
1034
+ cardContentFirst?.Status
1035
+ ?? cardContentFirst?.status
1036
+ ?? cardContentFirst?.approvalStatus
1037
+ ?? '';
1038
+ const status = typeof raw === 'string' ? raw.trim() : String(raw);
1039
+ const normalizedStatus = status.toLowerCase();
1040
+ switch (normalizedStatus) {
519
1041
  case RCS_STATUSES.approved:
520
1042
  setTemplateStatus(RCS_STATUSES.approved);
521
1043
  break;
522
1044
  case RCS_STATUSES.pending:
523
1045
  setTemplateStatus(RCS_STATUSES.pending);
524
1046
  break;
525
- case RCS_STATUSES.awaitingApproval:
1047
+ case RCS_STATUSES.awaitingApproval.toLowerCase():
526
1048
  setTemplateStatus(RCS_STATUSES.awaitingApproval);
527
1049
  break;
528
1050
  case RCS_STATUSES.unavailable:
@@ -531,6 +1053,9 @@ export const Rcs = (props) => {
531
1053
  case RCS_STATUSES.rejected:
532
1054
  setTemplateStatus(RCS_STATUSES.rejected);
533
1055
  break;
1056
+ case RCS_STATUSES.created:
1057
+ setTemplateStatus(RCS_STATUSES.created);
1058
+ break;
534
1059
  default:
535
1060
  setTemplateStatus(status);
536
1061
  break;
@@ -584,48 +1109,255 @@ export const Rcs = (props) => {
584
1109
  };
585
1110
 
586
1111
  useEffect(() => {
587
- const details = isFullMode ? rcsData?.templateDetails : templateData;
1112
+ const details = rcsHydrationDetails;
588
1113
  if (details && Object.keys(details).length > 0) {
589
- if (!isFullMode) {
590
- const tempCardVarMapped = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
591
- setCardVarMapped(tempCardVarMapped);
1114
+ // Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
1115
+ const cardFromVersions = get(
1116
+ details,
1117
+ 'versions.base.content.RCS.rcsContent.cardContent[0]',
1118
+ );
1119
+ const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
1120
+ const cardType = (rcsContent?.cardType || '').toString();
1121
+
1122
+ setEditFlow(true);
1123
+ setTemplateName(details?.name || details?.creativeName || '');
1124
+
1125
+ const cardFromTop = get(details, 'rcsContent.cardContent[0]');
1126
+ const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
1127
+ const cardVarMappedFromCardContent =
1128
+ card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
1129
+ ? card0.cardVarMapped
1130
+ : {};
1131
+ const cardVarMappedFromRootMirror =
1132
+ details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
1133
+ ? details.rcsCardVarMapped
1134
+ : {};
1135
+ // Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
1136
+ // nested versions.cardContent[0].cardVarMapped is dropped on reload.
1137
+ const mergedCardVarMappedFromPayload = {
1138
+ ...cardVarMappedFromRootMirror,
1139
+ ...cardVarMappedFromCardContent,
1140
+ };
1141
+ const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
1142
+ const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
1143
+ const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
1144
+ Object.keys(mergedCardVarMappedFromPayload)
1145
+ .sort()
1146
+ .reduce((accumulator, mapKey) => {
1147
+ accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
1148
+ return accumulator;
1149
+ }, {}),
1150
+ )}`;
1151
+ if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
1152
+ lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
1153
+ setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
1154
+ }
1155
+ const tokenListForMap = [
1156
+ ...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
1157
+ ...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
1158
+ ];
1159
+ const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
1160
+ // Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
1161
+ // getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
1162
+ const cardVarMappedBeforeCoalesce = isFullMode
1163
+ ? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
1164
+ : { ...mergedCardVarMappedFromPayload };
1165
+ const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
1166
+ cardVarMappedBeforeCoalesce,
1167
+ loadedTitleForMap,
1168
+ loadedDescForMap,
1169
+ rcsVarRegex,
1170
+ );
1171
+ const cardVarMappedAfterNumericSlotSync = !isFullMode
1172
+ ? syncCardVarMappedSemanticsFromSlots(
1173
+ cardVarMappedAfterCoalesce,
1174
+ loadedTitleForMap,
1175
+ loadedDescForMap,
1176
+ rcsVarRegex,
1177
+ )
1178
+ : cardVarMappedAfterCoalesce;
1179
+ const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
1180
+ // Pre-populate variable/tag mappings while opening an existing template in edit flows
1181
+ setCardVarMapped((previousVarMapState) => {
1182
+ const previousVarMap = previousVarMapState ?? {};
1183
+ if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
1184
+ const previousVarMapKeys = Object.keys(previousVarMap);
1185
+ const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
1186
+ if (previousVarMapKeys.length === nextVarMapKeys.length) {
1187
+ const allSlotValuesMatchPrevious = previousVarMapKeys.every(
1188
+ (key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
1189
+ );
1190
+ if (allSlotValuesMatchPrevious) return previousVarMapState;
1191
+ }
1192
+ return hydratedCardVarMappedResult;
1193
+ });
1194
+
1195
+ if (cardType.toUpperCase() === contentType.carousel) {
1196
+
1197
+ setTemplateType(contentType.carousel);
1198
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1199
+ // Library/campaign payloads can store cardSettings/cardContent at the flat top-level
1200
+ // `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
1201
+ // used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
1202
+ // silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
1203
+ // actually uploaded at and causing object-fit:cover to crop the preview image.
1204
+ const cardSettings = rcsContent?.cardSettings
1205
+ || get(details, 'rcsContent.cardSettings', {});
1206
+ const cardWidth = cardSettings?.cardWidth || SMALL;
1207
+ setSelectedCarouselWidth(cardWidth);
1208
+
1209
+ const cards = Array.isArray(rcsContent?.cardContent)
1210
+ ? rcsContent.cardContent
1211
+ : get(details, 'rcsContent.cardContent', []);
1212
+ const firstHeight = cards?.[0]?.media?.height || MEDIUM;
1213
+ setSelectedCarouselHeight(firstHeight);
1214
+ setSelectedCarousel(`${firstHeight}_${cardWidth}`);
1215
+ setActiveCarouselIndex('0');
1216
+
1217
+ if (cards.length > 1) {
1218
+ setCardVarMapped((previousVarMap) => cards.reduce((merged, c) => {
1219
+ const cardMap = c?.cardVarMapped;
1220
+ return cardMap != null && typeof cardMap === 'object' ? { ...merged, ...cardMap } : merged;
1221
+ }, { ...(previousVarMap || {}) }));
1222
+ }
1223
+
1224
+ const hydratedCards = cards.map((c = {}, idx) => {
1225
+ const mediaType = c.mediaType;
1226
+ const media = c.media || {};
1227
+ const mediaUrl = media.mediaUrl || '';
1228
+ const thumbUrl = media.thumbnailUrl || '';
1229
+ const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
1230
+ const suggestions = idx === 0 && rawSuggestions.length === 0
1231
+ ? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
1232
+ : rawSuggestions;
1233
+ return {
1234
+ title: c.title || '',
1235
+ description: c.description || '',
1236
+ mediaType,
1237
+ imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
1238
+ videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
1239
+ videoSrc: mediaUrl,
1240
+ previewUrl: thumbUrl,
1241
+ videoThumbnail: thumbUrl,
1242
+ videoName: c?.media?.videoName || '',
1243
+ } : {},
1244
+ thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
1245
+ suggestions,
1246
+ };
1247
+ });
1248
+ setCarouselData(
1249
+ hydratedCards.length > 0
1250
+ ? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
1251
+ : [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
1252
+ );
1253
+ setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
1254
+
1255
+ // Status bar uses first card's Status, keep existing behavior.
1256
+ if (isHostInfoBip) {
1257
+ setTemplateStatus('');
1258
+ } else {
1259
+ const firstCard = cards?.[0] || {};
1260
+ const cardForCarouselStatus = {
1261
+ ...firstCard,
1262
+ Status:
1263
+ firstCard.Status
1264
+ ?? firstCard.status
1265
+ ?? firstCard.approvalStatus
1266
+ ?? get(details, 'templateStatus')
1267
+ ?? get(details, 'approvalStatus')
1268
+ ?? get(details, 'creativeStatus')
1269
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1270
+ ?? '',
1271
+ };
1272
+ templateStatusHelper(cardForCarouselStatus);
1273
+ }
1274
+
1275
+ // Hydrate SMS fallback for carousel — same logic as for non-carousel types below.
1276
+ // Without this the early `return` skips fallback hydration entirely, so reopening
1277
+ // a carousel template never restores the saved SMS fallback data.
1278
+ applySmsFallbackHydration({
1279
+ details,
1280
+ isFullMode,
1281
+ lastSmsFallbackHydrationKeyRef,
1282
+ setSmsFallbackData,
1283
+ });
1284
+
1285
+ return;
592
1286
  }
593
- const mediaType = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
594
- if (mediaType === RCS_MEDIA_TYPES.NONE) {
1287
+
1288
+ const mediaType =
1289
+ card0.mediaType
1290
+ || get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
1291
+ if (cardType.toUpperCase() !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
595
1292
  setTemplateType(contentType.text_message);
596
- } else {
1293
+ } else if (cardType.toUpperCase() !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
597
1294
  setTemplateType(contentType.rich_card);
598
1295
  }
599
- setEditFlow(true);
600
- setTemplateName(details.name || '');
601
- const loadedTitle = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].title', '');
602
- const loadedDesc = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].description', '');
603
- const loadedMap = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
604
- const normalizedTitle = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedTitle, loadedMap) : loadedTitle;
605
- const normalizedDesc = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedDesc, loadedMap) : loadedDesc;
1296
+
1297
+ const loadedTitle = loadedTitleForMap;
1298
+ const loadedDesc = loadedDescForMap;
1299
+ const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
1300
+ loadedTitle,
1301
+ loadedDesc,
1302
+ isFullMode,
1303
+ cardVarMappedAfterHydration: hydratedCardVarMappedResult,
1304
+ rcsVarRegex,
1305
+ });
606
1306
  setTemplateTitle(normalizedTitle);
607
1307
  setTemplateDesc(normalizedDesc);
608
- setSuggestions(get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []));
609
- templateStatusHelper(details);
610
- const mediaData = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
611
- const cardSettings = get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '');
1308
+ setSuggestions(
1309
+ Array.isArray(card0.suggestions)
1310
+ ? card0.suggestions
1311
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
1312
+ );
1313
+ const cardForStatus = {
1314
+ ...card0,
1315
+ Status:
1316
+ card0.Status
1317
+ ?? card0.status
1318
+ ?? card0.approvalStatus
1319
+ ?? get(details, 'templateStatus')
1320
+ ?? get(details, 'approvalStatus')
1321
+ ?? get(details, 'creativeStatus')
1322
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1323
+ ?? '',
1324
+ };
1325
+ if (isHostInfoBip) {
1326
+ setTemplateStatus('');
1327
+ } else {
1328
+ templateStatusHelper(cardForStatus);
1329
+ }
1330
+ const mediaData =
1331
+ card0.media != null && card0.media !== ''
1332
+ ? card0.media
1333
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
1334
+ const cardSettings =
1335
+ get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
1336
+ || get(details, 'rcsContent.cardSettings', '');
612
1337
  setMediaData(mediaData, mediaType, cardSettings);
613
1338
  if (details?.edit) {
614
1339
  const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
615
1340
  setRcsAccount(rcsAccountId);
616
1341
  }
1342
+
1343
+ applySmsFallbackHydration({
1344
+ details,
1345
+ isFullMode,
1346
+ lastSmsFallbackHydrationKeyRef,
1347
+ setSmsFallbackData,
1348
+ });
617
1349
  }
618
- }, [rcsData, templateData, isFullMode, isEditFlow]);
619
-
1350
+ }, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
620
1351
 
621
1352
  useEffect(() => {
622
1353
  if (templateType === contentType.text_message) {
623
1354
  setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
624
- setTemplateTitle('');
625
- setTemplateTitleError('');
1355
+ // Full-mode create only: switching to plain text clears draft title/media. Never clear when
1356
+ // hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
626
1357
  if (!isEditFlow && isFullMode) {
1358
+ setTemplateTitle('');
1359
+ setTemplateTitleError('');
627
1360
  setUpdateRcsImageSrc('');
628
- setUpdateRcsVideoSrc({});
629
1361
  setRcsVideoSrc({});
630
1362
  setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
631
1363
  }
@@ -649,23 +1381,22 @@ export const Rcs = (props) => {
649
1381
  }, [templateType, isEditFlow, isFullMode]);
650
1382
 
651
1383
  useEffect(() => {
652
- if (!showDltContainer) {
653
- const { type, module } = location.query || {};
654
- const isEmbedded = type === EMBEDDED;
655
- const context = isEmbedded ? module : DEFAULT;
656
- const embedded = isEmbedded ? type : FULL;
657
- const query = {
658
- layout: SMS,
659
- type: TAG,
660
- context,
661
- embedded,
662
- };
663
- if (getDefaultTags) {
664
- query.context = getDefaultTags;
665
- }
666
- globalActions.fetchSchemaForEntity(query);
1384
+ const { type, module } = location.query || {};
1385
+ const isEmbedded = type === EMBEDDED;
1386
+ // Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
1387
+ const context = isEmbedded ? module : 'outbound';
1388
+ const embedded = isEmbedded ? type : FULL;
1389
+ const query = {
1390
+ layout: SMS,
1391
+ type: TAG,
1392
+ context,
1393
+ embedded,
1394
+ };
1395
+ if (getDefaultTags) {
1396
+ query.context = getDefaultTags;
667
1397
  }
668
- }, [showDltContainer]);
1398
+ fetchTagSchemaIfNewQuery(query);
1399
+ }, [fetchTagSchemaIfNewQuery]);
669
1400
 
670
1401
  useEffect(() => {
671
1402
  let tag = get(metaEntities, `tags.standard`, []);
@@ -688,16 +1419,128 @@ export const Rcs = (props) => {
688
1419
  context,
689
1420
  embedded,
690
1421
  };
1422
+ if (getDefaultTags) {
1423
+ query.context = getDefaultTags;
1424
+ }
1425
+ fetchTagSchemaIfNewQuery(query);
691
1426
  globalActions.fetchSchemaForEntity(query);
692
1427
  };
693
1428
 
694
- const onTagSelect = (data, areaId) => {
695
- if (!areaId) return;
696
- const sep = areaId.lastIndexOf('_');
1429
+ const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
1430
+ if (!templateStr || !numericVarName || !tagName) return templateStr;
1431
+ const placeholderRegex = buildRcsNumericMustachePlaceholderRegex(numericVarName);
1432
+ return templateStr.replace(placeholderRegex, `{{${tagName}}}`);
1433
+ };
1434
+
1435
+ const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
1436
+ if (!varSegmentCompositeDomId) return;
1437
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
1438
+ if (underscoreIndexInCompositeId === -1) return;
1439
+ const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
1440
+ if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
1441
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
1442
+ const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
1443
+ if (!semanticOrNumericVarName) return;
1444
+ const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
1445
+ const templateStringForField =
1446
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
1447
+ const titleOrMessageFieldType =
1448
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
1449
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
1450
+ varSegmentCompositeDomId,
1451
+ templateStringForField,
1452
+ titleOrMessageFieldType,
1453
+ );
1454
+ const cardVarMappedNumericSlotKey =
1455
+ globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
1456
+ ? String(globalVarSlotIndexZeroBased + 1)
1457
+ : null;
1458
+
1459
+ setCardVarMapped((previousCardVarMapped) => {
1460
+ const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
1461
+ if (isNumericPlaceholderSlot) {
1462
+ const existingValueBeforeAppend = (
1463
+ previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
1464
+ ).toString();
1465
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1466
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1467
+ updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
1468
+ } else {
1469
+ // Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
1470
+ // "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
1471
+ // read/write the global numeric slot only and drop the shared semantic key.
1472
+ const existingValueBeforeAppend = cardVarMappedNumericSlotKey
1473
+ ? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
1474
+ : String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
1475
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1476
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1477
+ if (cardVarMappedNumericSlotKey) {
1478
+ updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
1479
+ // Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
1480
+ const fieldOffset = titleOrMessageFieldType === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
1481
+ const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
1482
+ let varOrdinal = 0;
1483
+ for (let i = 0; i < fieldSegments.length; i++) {
1484
+ if (rcsVarTestRegex.test(fieldSegments[i])) {
1485
+ const slotVarName = getVarNameFromToken(fieldSegments[i]);
1486
+ const globalSlot = fieldOffset + varOrdinal;
1487
+ varOrdinal += 1;
1488
+ if (slotVarName === semanticOrNumericVarName && globalSlot !== globalVarSlotIndexZeroBased) {
1489
+ updatedCardVarMapped[String(globalSlot + 1)] = mappedValueAfterAppendingTag;
1490
+ }
1491
+ }
1492
+ }
1493
+ } else {
1494
+ updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
1495
+ }
1496
+ }
1497
+ return updatedCardVarMapped;
1498
+ });
1499
+
1500
+ if (
1501
+ isNumericPlaceholderSlot
1502
+ && (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
1503
+ ) {
1504
+ if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
1505
+ setTemplateTitle((previousTitle) => {
1506
+ const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
1507
+ previousTitle || '',
1508
+ semanticOrNumericVarName,
1509
+ selectedTagNameFromPicker,
1510
+ );
1511
+ if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
1512
+ setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
1513
+ // 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
1514
+ setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
1515
+ return titleAfterReplacingNumericPlaceholder;
1516
+ });
1517
+ } else {
1518
+ setTemplateDesc((previousDescription) => {
1519
+ const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
1520
+ previousDescription || '',
1521
+ semanticOrNumericVarName,
1522
+ selectedTagNameFromPicker,
1523
+ );
1524
+ if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
1525
+ return previousDescription;
1526
+ }
1527
+ setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
1528
+ setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
1529
+ return descriptionAfterReplacingNumericPlaceholder;
1530
+ });
1531
+ }
1532
+ }
1533
+ };
1534
+
1535
+ const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
1536
+
1537
+ const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
1538
+
1539
+ const onCarouselTagSelect = (data) => {
1540
+ if (!carouselFocusedVarId) return;
1541
+ const sep = carouselFocusedVarId.lastIndexOf('_');
697
1542
  if (sep === -1) return;
698
- const numId = Number(areaId.slice(sep + 1));
699
- if (isNaN(numId)) return;
700
- const token = areaId.slice(0, sep);
1543
+ const token = carouselFocusedVarId.slice(0, sep);
701
1544
  const variableName = getVarNameFromToken(token);
702
1545
  if (!variableName) return;
703
1546
  setCardVarMapped((prev) => {
@@ -710,18 +1553,6 @@ export const Rcs = (props) => {
710
1553
  });
711
1554
  };
712
1555
 
713
- const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
714
-
715
- const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
716
-
717
- const onTagSelectFallback = (data) => {
718
- const tempMsg = `${fallbackMessage}{{${data}}}`;
719
- const error = fallbackMessageErrorHandler(tempMsg);
720
- setFallbackMessage(tempMsg);
721
- setFallbackMessageError(error);
722
- };
723
-
724
-
725
1556
  //removing optout tag for rcs
726
1557
  const getRcsTags = () => {
727
1558
  const tempTags = cloneDeep(tags);
@@ -733,15 +1564,14 @@ export const Rcs = (props) => {
733
1564
  };
734
1565
  // tag Code end
735
1566
 
736
- const renderLabel = (value, showLabel, desc) => {
737
- const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
1567
+ const renderLabel = (value, desc) => {
738
1568
  return (
739
1569
  <>
740
- <RcsLabel>
1570
+ <div className="rcs-form-section-heading">
741
1571
  <CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
742
- </RcsLabel>
1572
+ </div>
743
1573
  {desc && (
744
- <CapLabel type="label3" style={{ marginBottom: '17px' }}>
1574
+ <CapLabel type="label3" className="rcs-form-field-caption">
745
1575
  {formatMessage(messages[desc])}
746
1576
  </CapLabel>
747
1577
  )}
@@ -760,13 +1590,8 @@ export const Rcs = (props) => {
760
1590
  },
761
1591
  {
762
1592
  value: contentType.carousel,
763
- label: (
764
- <CapTooltip title={formatMessage(messages.disabledCarouselTooltip)}>
765
- {formatMessage(messages.carousel)}
766
- </CapTooltip>
767
- ),
768
- disabled: true,
769
- },
1593
+ label: formatMessage(messages.carousel),
1594
+ }
770
1595
  ];
771
1596
 
772
1597
  const onTemplateNameChange = ({ target: { value } }) => {
@@ -777,6 +1602,10 @@ export const Rcs = (props) => {
777
1602
 
778
1603
  const onTemplateTypeChange = ({ target: { value } }) => {
779
1604
  setTemplateType(value);
1605
+ // Carousel has per-card media; keep template-level media type neutral.
1606
+ if (value === contentType.carousel) {
1607
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1608
+ }
780
1609
  };
781
1610
 
782
1611
 
@@ -797,8 +1626,39 @@ export const Rcs = (props) => {
797
1626
  const onTemplateMediaTypeChange = ({ target: { value } }) => {
798
1627
  setTemplateMediaType(value);
799
1628
  };
800
-
801
-
1629
+ const renderedRCSEditMessage = (descArray, type) => {
1630
+ const renderArray = [];
1631
+ if (descArray?.length) {
1632
+ descArray.forEach((elem, index) => {
1633
+ if (rcsVarTestRegex.test(elem)) {
1634
+ // Variable input
1635
+ renderArray.push(
1636
+ <TextArea
1637
+ id={`${elem}_${index}`}
1638
+ key={`${elem}_${index}`}
1639
+ placeholder={`enter the value for ${elem}`}
1640
+ autosize={{ minRows: 1, maxRows: 3 }}
1641
+ onChange={e => textAreaValueChange(e, type)}
1642
+ value={textAreaValue(index, type)}
1643
+ onFocus={(e) => setTextAreaId(e, type)}
1644
+ />
1645
+ );
1646
+ } else if (elem) {
1647
+ // Static text
1648
+ renderArray.push(
1649
+ <TextArea
1650
+ key={`static_${index}`}
1651
+ value={elem}
1652
+ autosize={{ minRows: 1, maxRows: 3 }}
1653
+ disabled
1654
+ className="rcs-edit-template-message-static-textarea"
1655
+ />
1656
+ );
1657
+ }
1658
+ });
1659
+ }
1660
+ return renderArray;
1661
+ };
802
1662
  const onTemplateTitleChange = ({ target: { value } }) => {
803
1663
  let errorMessage = false;
804
1664
  if (templateType === contentType.rich_card && !value.trim()) {
@@ -814,7 +1674,7 @@ export const Rcs = (props) => {
814
1674
 
815
1675
  const onTemplateDescChange = ({ target: { value } }) => {
816
1676
  let errorMessage = false;
817
- if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
1677
+ if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
818
1678
  errorMessage = formatMessage(messages.templateMessageLengthError);
819
1679
  } else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
820
1680
  errorMessage = formatMessage(messages.templateMessageLengthError);
@@ -830,16 +1690,16 @@ export const Rcs = (props) => {
830
1690
 
831
1691
  const templateDescErrorHandler = (value) => {
832
1692
  let errorMessage = false;
833
- const { isBraceError } = validateTags({
1693
+ const { unsupportedTags, isBraceError } = validateTags({
834
1694
  content: value,
835
1695
  tagsParam: tags,
1696
+ injectedTagsParams: injectedTags,
836
1697
  location,
837
1698
  tagModule: getDefaultTags,
838
- isFullMode,
839
1699
  }) || {};
840
1700
 
841
1701
  const maxLength = templateType === contentType.text_message
842
- ? RCS_TEXT_MESSAGE_MAX_LENGTH
1702
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
843
1703
  : RCS_RICH_CARD_MAX_LENGTH;
844
1704
 
845
1705
  if (value === '' && isMediaTypeText) {
@@ -855,19 +1715,6 @@ export const Rcs = (props) => {
855
1715
  };
856
1716
 
857
1717
 
858
- const onFallbackMessageChange = ({ target: { value } }) => {
859
- const error = fallbackMessageErrorHandler(value);
860
- setFallbackMessage(value);
861
- setFallbackMessageError(error);
862
- };
863
-
864
- const fallbackMessageErrorHandler = (value) => {
865
- if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
866
- return formatMessage(messages.fallbackMsgLenError);
867
- }
868
- return false;
869
- };
870
-
871
1718
  // Check for forbidden characters: square brackets [] and single curly braces {}
872
1719
  const forbiddenCharactersValidation = (value) => {
873
1720
  if (!value) return false;
@@ -912,53 +1759,43 @@ export const Rcs = (props) => {
912
1759
  if(!isFullMode){
913
1760
  return false;
914
1761
  }
915
- if (!/^\w+$/.test(paramName)) {
1762
+ // Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
1763
+ if (!/^[\w.]+$/.test(paramName)) {
916
1764
  return formatMessage(messages.unknownCharactersError);
917
1765
  }
918
1766
  }
919
1767
  return false;
920
1768
  };
921
-
922
- const templateHeaderErrorHandler = (value) => {
923
- let errorMessage = false;
924
- if (value?.length > TEMPLATE_HEADER_MAX_LENGTH) {
925
- errorMessage = formatMessage(messages.templateHeaderLengthError);
926
- } else {
927
- errorMessage = variableErrorHandling(value);
928
- }
929
- return errorMessage;
930
- };
931
-
932
-
933
- const templateMessageErrorHandler = (value) => {
934
- let errorMessage = false;
935
- if (value === '') {
936
- errorMessage = formatMessage(messages.emptyTemplateMessageErrorMessage);
937
- } else if (
938
- value?.length
939
- > TEMPLATE_MESSAGE_MAX_LENGTH
940
- ) {
941
- errorMessage = formatMessage(messages.templateMessageLengthError);
942
- } else {
943
- errorMessage = variableErrorHandling(value);
944
- }
945
- return errorMessage;
946
- };
947
-
948
1769
 
949
1770
  const onMessageAddVar = () => {
950
- onAddVar(MESSAGE_TEXT, templateDesc, rcsVarRegex);
1771
+ onAddVar(templateDesc);
951
1772
  };
952
1773
 
953
- const onAddVar = (type, messageContent, regex) => {
954
- // Always append the next variable at the end, like WhatsApp
955
- const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
956
- const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
1774
+ /**
1775
+ * Returns the smallest positive integer not already used as a `{{N}}` variable
1776
+ * in either the title or description fields, or null if the limit (19) is reached.
1777
+ * Scans both fields so title and description vars never share the same number
1778
+ * (duplicate numbers would share a cardVarMapped key and bleed values across fields).
1779
+ */
1780
+ const getNextRcsNumericVarNumber = (titleStr, descStr) => {
1781
+ const allExistingVars = [
1782
+ ...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
1783
+ ...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
1784
+ ];
1785
+ const existingNumbers = allExistingVars.flatMap(varToken => {
1786
+ const numericMatch = varToken.match(/\d+/);
1787
+ return numericMatch ? [parseInt(numericMatch[0], 10)] : [];
1788
+ });
957
1789
  let nextNumber = 1;
958
1790
  while (existingNumbers.includes(nextNumber)) {
959
1791
  nextNumber++;
960
1792
  }
961
- if (nextNumber > 19) {
1793
+ return nextNumber > 19 ? null : nextNumber;
1794
+ };
1795
+
1796
+ const onAddVar = (messageContent) => {
1797
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
1798
+ if (nextNumber === null) {
962
1799
  return;
963
1800
  }
964
1801
  const nextVar = `{{${nextNumber}}}`;
@@ -970,15 +1807,13 @@ const onAddVar = (type, messageContent, regex) => {
970
1807
  };
971
1808
 
972
1809
  const onTitleAddVar = () => {
973
- // Always append the next variable at the end, like WhatsApp
1810
+ // Scan both title AND description so the new title var number doesn't
1811
+ // duplicate a number already used in the description. Duplicate numeric
1812
+ // names would share the same cardVarMapped semantic key, causing the
1813
+ // description slot to reflect the title slot value and vice-versa.
974
1814
  const messageContent = templateTitle;
975
- const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
976
- const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
977
- let nextNumber = 1;
978
- while (existingNumbers.includes(nextNumber)) {
979
- nextNumber++;
980
- }
981
- if (nextNumber > 19) {
1815
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
1816
+ if (nextNumber === null) {
982
1817
  return;
983
1818
  }
984
1819
  const nextVar = `{{${nextNumber}}}`;
@@ -990,26 +1825,17 @@ const onTitleAddVar = () => {
990
1825
  setTemplateTitleError(error);
991
1826
  };
992
1827
 
993
-
994
- const splitTemplateVarString = (str) => {
995
- if (!str) return [];
996
- const validVarArr = str.match(rcsVarRegex) || [];
997
- const templateVarArray = [];
998
- let content = str;
999
- while (content?.length !== 0) {
1000
- const index = content.indexOf(validVarArr?.[0]);
1001
- if (index !== -1) {
1002
- templateVarArray.push(content.substring(0, index));
1003
- templateVarArray.push(validVarArr?.[0]);
1004
- content = content.substring(index + validVarArr?.[0]?.length, content?.length);
1005
- validVarArr?.shift();
1006
- } else {
1007
- templateVarArray.push(content);
1008
- break;
1009
- }
1010
- }
1011
- return templateVarArray.filter(Boolean);
1012
- };
1828
+ const appendVarToCarouselField = (cardIndex, fieldName) => {
1829
+ const token = getNextCarouselVarToken(carouselData);
1830
+ if (!token) return;
1831
+ setCarouselData((prev = []) => {
1832
+ const updated = cloneDeep(prev);
1833
+ if (!updated[cardIndex]) return prev;
1834
+ const current = (updated[cardIndex][fieldName] || '').toString();
1835
+ updated[cardIndex][fieldName] = `${current}${token}`;
1836
+ return updated;
1837
+ });
1838
+ };
1013
1839
 
1014
1840
  const textAreaValue = (idValue, type) => {
1015
1841
  if (idValue >= 0) {
@@ -1025,6 +1851,46 @@ const splitTemplateVarString = (str) => {
1025
1851
  return "";
1026
1852
  };
1027
1853
 
1854
+ // Carousel: render variable-value editor for a given template string (title/description).
1855
+ // This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
1856
+ const renderCarouselEditMessage = (templateStr) => {
1857
+ const renderArray = [];
1858
+ const templateArr = splitTemplateVarString(templateStr);
1859
+ if (templateArr?.length) {
1860
+ templateArr.forEach((elem, index) => {
1861
+ if (rcsVarTestRegex.test(elem)) {
1862
+ const varName = getVarNameFromToken(elem);
1863
+ renderArray.push(
1864
+ <div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
1865
+ <TextArea
1866
+ id={`${elem}_${index}`}
1867
+ placeholder={`enter the value for ${elem}`}
1868
+ autosize={{ minRows: 1, maxRows: 3 }}
1869
+ onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
1870
+ value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
1871
+ onFocus={(e) => {
1872
+ const id = e?.target?.id || e?.currentTarget?.id || '';
1873
+ setCarouselFocusedVarId(id);
1874
+ }}
1875
+ />
1876
+ </div>
1877
+ );
1878
+ } else if (elem) {
1879
+ renderArray.push(
1880
+ <CapHeading
1881
+ key={`static_${index}`}
1882
+ type="h4"
1883
+ className="rcs-edit-template-message-split"
1884
+ >
1885
+ {elem}
1886
+ </CapHeading>
1887
+ );
1888
+ }
1889
+ });
1890
+ }
1891
+ return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
1892
+ };
1893
+
1028
1894
  const textAreaValueChange = (e, type) => {
1029
1895
  const value = e?.target?.value ?? '';
1030
1896
  const id = e?.target?.id || e?.currentTarget?.id || '';
@@ -1044,7 +1910,9 @@ const splitTemplateVarString = (str) => {
1044
1910
  };
1045
1911
 
1046
1912
  const setTextAreaId = (e, type) => {
1047
- const id = e?.target?.id || e?.currentTarget?.id || '';
1913
+ // VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
1914
+ // have an `.target.id` shape. Support both.
1915
+ const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
1048
1916
  if (!id) return;
1049
1917
  if (type === TITLE_TEXT) setTitleTextAreaId(id);
1050
1918
  else setDescTextAreaId(id);
@@ -1063,10 +1931,12 @@ const splitTemplateVarString = (str) => {
1063
1931
  </CapRow>
1064
1932
  }
1065
1933
  description={
1066
- <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
1934
+ !isEditFlow && (
1935
+ <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
1936
+ )
1067
1937
  }
1068
1938
  />
1069
-
1939
+
1070
1940
  <CapActionButton
1071
1941
  buttonType={buttonType}
1072
1942
  updateButtonChange={updateButtonChange}
@@ -1075,44 +1945,90 @@ const splitTemplateVarString = (str) => {
1075
1945
  isEditFlow={isEditFlow}
1076
1946
  isFullMode={isFullMode}
1077
1947
  maxButtons={MAX_BUTTONS}
1078
- />
1948
+ host={hostName}
1949
+ location={location}
1950
+ tags={getRcsTags()}
1951
+ injectedTags={injectedTags || {}}
1952
+ selectedOfferDetails={selectedOfferDetails}
1953
+ onContextChange={handleOnTagsContextChange}
1954
+ />
1079
1955
  </>
1080
1956
  );
1081
1957
  };
1082
1958
 
1083
- const renderedRCSEditMessage = (descArray, type) => {
1084
- const renderArray = [];
1085
- if (descArray?.length) {
1086
- descArray.forEach((elem, index) => {
1087
- if (rcsVarTestRegex.test(elem)) {
1088
- // Variable input
1089
- renderArray.push(
1090
- <TextArea
1091
- id={`${elem}_${index}`}
1092
- key={`${elem}_${index}`}
1093
- placeholder={`enter the value for ${elem}`}
1094
- autosize={{ minRows: 1, maxRows: 3 }}
1095
- onChange={e => textAreaValueChange(e, type)}
1096
- value={textAreaValue(index, type)}
1097
- onFocus={(e) => setTextAreaId(e, type)}
1098
- />
1099
- );
1100
- } else if (elem) {
1101
- // Static text
1102
- renderArray.push(
1103
- <TextArea
1104
- key={`static_${index}`}
1105
- value={elem}
1106
- autosize={{ minRows: 1, maxRows: 3 }}
1107
- disabled
1108
- className="rcs-edit-template-message-static-textarea"
1109
- style={{ background: '#fafafa', color: '#888' }}
1110
- />
1111
- );
1959
+ const getRcsValueMap = (fieldTemplateString, fieldType) => {
1960
+ if (!fieldTemplateString) return {};
1961
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
1962
+ const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
1963
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
1964
+ const segmentIdToResolvedValue = {};
1965
+ let varOrdinal = 0;
1966
+ templateSegments.forEach((segmentToken, segmentIndexInField) => {
1967
+ if (rcsVarTestRegex.test(segmentToken)) {
1968
+ const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
1969
+ const varName = getVarNameFromToken(segmentToken);
1970
+ const globalSlot = slotOffset + varOrdinal;
1971
+ varOrdinal += 1;
1972
+ segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
1973
+ cardVarMapped,
1974
+ varName,
1975
+ globalSlot,
1976
+ isEditLike,
1977
+ rcsSpanningSemanticVarNames.has(varName),
1978
+ );
1979
+ }
1980
+ });
1981
+ return segmentIdToResolvedValue;
1982
+ };
1983
+
1984
+ const titleVarSegmentValueMapById = useMemo(
1985
+ () => getRcsValueMap(templateTitle, TITLE_TEXT),
1986
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
1987
+ );
1988
+ const descriptionVarSegmentValueMapById = useMemo(
1989
+ () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
1990
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
1991
+ );
1992
+
1993
+ const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
1994
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
1995
+ if (underscoreIndexInCompositeId === -1) return;
1996
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
1997
+ const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
1998
+ if (variableName === undefined || variableName === null || variableName === '') return;
1999
+ const isInvalidValue = value?.trim() === '';
2000
+ const coercedSlotValue = isInvalidValue ? '' : value;
2001
+ const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
2002
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
2003
+ varSegmentCompositeDomId,
2004
+ templateStringForField,
2005
+ type,
2006
+ );
2007
+ const fieldOffset = type === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
2008
+ setCardVarMapped((previousCardVarMapped) => {
2009
+ const updatedCardVarMapped = { ...previousCardVarMapped };
2010
+ // Remove stale semantic key: keeping it causes every other slot sharing the same
2011
+ // variable name (e.g. {{adv}} in both title and description) to read the same value
2012
+ // via the semantic-key fallback in resolveCardVarMappedSlotValue.
2013
+ delete updatedCardVarMapped[variableName];
2014
+ if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
2015
+ updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
2016
+ // Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
2017
+ const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
2018
+ let varOrdinal = 0;
2019
+ for (let i = 0; i < fieldSegments.length; i++) {
2020
+ if (rcsVarTestRegex.test(fieldSegments[i])) {
2021
+ const slotVarName = getVarNameFromToken(fieldSegments[i]);
2022
+ const globalSlot = fieldOffset + varOrdinal;
2023
+ varOrdinal += 1;
2024
+ if (slotVarName === variableName && globalSlot !== globalVarSlotIndexZeroBased) {
2025
+ updatedCardVarMapped[String(globalSlot + 1)] = coercedSlotValue;
2026
+ }
2027
+ }
1112
2028
  }
1113
- });
1114
- }
1115
- return renderArray;
2029
+ }
2030
+ return updatedCardVarMapped;
2031
+ });
1116
2032
  };
1117
2033
 
1118
2034
  const renderTextComponent = () => {
@@ -1132,6 +2048,7 @@ const splitTemplateVarString = (str) => {
1132
2048
  }
1133
2049
  suffix={
1134
2050
  <>
2051
+
1135
2052
  {(isEditFlow || !isFullMode) ? (
1136
2053
  <TagList
1137
2054
  label={formatMessage(globalMessages.addLabels)}
@@ -1148,18 +2065,28 @@ const splitTemplateVarString = (str) => {
1148
2065
  type="flat"
1149
2066
  isAddBtn
1150
2067
  onClick={onTitleAddVar}
1151
- disabled={!!templateTitleError}
2068
+ disabled={templateTitleError}
1152
2069
  >
1153
2070
  {formatMessage(messages.addVar)}
1154
2071
  </CapButton>
1155
- )}
2072
+ )}
1156
2073
  </>
1157
- }
2074
+ }
2075
+ />
2076
+
2077
+ {(isEditFlow || !isFullMode) ? (
2078
+ <VarSegmentMessageEditor
2079
+ key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
2080
+ templateString={templateTitle}
2081
+ valueMap={titleVarSegmentValueMapById}
2082
+ onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
2083
+ onFocus={(id) => setTitleTextAreaId(id)}
2084
+ varRegex={rcsVarRegex}
2085
+ placeholderPrefix=""
2086
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
1158
2087
  />
1159
- <div className="rcs_text_area_wrapper">
1160
- {(isEditFlow || !isFullMode) ? (
1161
- renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
1162
2088
  ) : (
2089
+ <div className="rcs_text_area_wrapper">
1163
2090
  <CapInput
1164
2091
  className={`rcs-template-title-input ${
1165
2092
  !isTemplateApproved ? "rcs-edit-disabled" : ""
@@ -1172,8 +2099,8 @@ const splitTemplateVarString = (str) => {
1172
2099
  errorMessage={templateTitleError}
1173
2100
  disabled={isEditFlow || !isFullMode}
1174
2101
  />
2102
+ </div>
1175
2103
  )}
1176
- </div>
1177
2104
  {(isEditFlow || !isFullMode) && templateTitleError && (
1178
2105
  <CapError className="rcs-template-title-error">
1179
2106
  {templateTitleError}
@@ -1181,7 +2108,7 @@ const splitTemplateVarString = (str) => {
1181
2108
  )}
1182
2109
  {!isEditFlow && isFullMode && renderTitleCharacterCount()}
1183
2110
  </>
1184
- )}
2111
+ )}
1185
2112
 
1186
2113
  {/* Template Message */}
1187
2114
  <CapRow useLegacy id="rcs-template-message-label">
@@ -1219,9 +2146,21 @@ const splitTemplateVarString = (str) => {
1219
2146
  />
1220
2147
  </CapRow>
1221
2148
  <CapRow useLegacy className="rcs-create-template-message-input">
1222
- <div className="rcs_text_area_wrapper">
1223
- {(isEditFlow || !isFullMode)
1224
- ? renderedRCSEditMessage(splitTemplateVarString(templateDesc), MESSAGE_TEXT)
2149
+ {/* 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. */}
2150
+ <CapRow className="rcs_text_area_wrapper">
2151
+ {(isEditFlow || !isFullMode)?
2152
+ (
2153
+ <VarSegmentMessageEditor
2154
+ key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
2155
+ templateString={templateDesc}
2156
+ valueMap={descriptionVarSegmentValueMapById}
2157
+ onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
2158
+ onFocus={(id) => setDescTextAreaId(id)}
2159
+ varRegex={rcsVarRegex}
2160
+ placeholderPrefix=""
2161
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
2162
+ />
2163
+ )
1225
2164
  : (
1226
2165
  <>
1227
2166
  <TextArea
@@ -1259,13 +2198,15 @@ const splitTemplateVarString = (str) => {
1259
2198
  </>
1260
2199
  )
1261
2200
  }
1262
- </div>
2201
+ </CapRow>
1263
2202
  {(isEditFlow || !isFullMode) && templateDescError && (
1264
2203
  <CapError className="rcs-template-message-error">
1265
2204
  {templateDescError}
1266
2205
  </CapError>
1267
2206
  )}
1268
- {!isEditFlow && isFullMode && renderDescriptionCharacterCount()}
2207
+ {(isEditFlow || !isFullMode)
2208
+ ? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
2209
+ : (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
1269
2210
  {!isFullMode && hasTag() && (
1270
2211
  <CapAlert
1271
2212
  message={
@@ -1279,24 +2220,13 @@ const splitTemplateVarString = (str) => {
1279
2220
  />
1280
2221
  )}
1281
2222
  </CapRow>
1282
- {renderButtonComponent()}
2223
+ {((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
2224
+ renderButtonComponent()}
1283
2225
  </>
1284
2226
 
1285
2227
  );
1286
2228
  };
1287
2229
 
1288
-
1289
- const fallbackSmsLength = () => (
1290
- <CapLabel type="label1" className="fallback-sms-length">
1291
- {formatMessage(messages.totalCharacters, {
1292
- smsCount: Math.ceil(
1293
- fallbackMessage?.length / FALLBACK_MESSAGE_MAX_LENGTH,
1294
- ),
1295
- number: fallbackMessage?.length,
1296
- })}
1297
- </CapLabel>
1298
- );
1299
-
1300
2230
  // Get character count for title (rich card only)
1301
2231
  const getTitleCharacterCount = () => {
1302
2232
  if (templateType === contentType.text_message) return 0;
@@ -1311,7 +2241,7 @@ const splitTemplateVarString = (str) => {
1311
2241
  // Get max length for description based on template type
1312
2242
  const getDescriptionMaxLength = () => {
1313
2243
  return templateType === contentType.text_message
1314
- ? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
2244
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
1315
2245
  : RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
1316
2246
  };
1317
2247
 
@@ -1374,287 +2304,34 @@ const splitTemplateVarString = (str) => {
1374
2304
  return false;
1375
2305
  };
1376
2306
 
1377
- //adding creative dlt fallback sms handlers
1378
- const addDltMsgHandler = () => {
1379
- setShowDltContainer(true);
1380
- setDltMode(RCS_DLT_MODE.TEMPLATES);
1381
- setDltEditData({});
1382
- setFallbackMessage('');
1383
- };
1384
-
1385
- const closeDltContainerHandler = () => {
1386
- setShowDltContainer(false);
1387
- setDltMode('');
1388
- templatesActions.getAllTemplates('Rcs', { page: 1, perPage: 25 });
1389
- };
1390
-
1391
- const rcsDltEditSelectHandler = (data) => {
1392
- setShowDltContainer(true);
1393
- setDltMode(RCS_DLT_MODE.EDIT);
1394
- setDltEditData(data);
1395
- };
1396
-
1397
- const rcsDltEditCompletionHandler = (data) => {
1398
- const tempData = {
1399
- versions: {
1400
- // eslint-disable-next-line no-undef
1401
- base: data?.value?.base || {},
1402
- },
1403
- };
1404
- const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
1405
- '',
1406
- );
1407
- closeDltContainerHandler();
1408
- setDltEditData(tempData);
1409
- setFallbackMessage(fallMsg);
1410
- setFallbackMessageError(fallMsg?.length > FALLBACK_MESSAGE_MAX_LENGTH);
1411
- setShowDltCard(true);
1412
- };
1413
-
1414
- const rcsDltCardDeleteHandler = () => {
1415
- closeDltContainerHandler();
1416
- setDltEditData({});
1417
- setFallbackMessage('');
1418
- setFallbackMessageError(false);
1419
- setShowDltCard(false);
1420
- };
1421
-
1422
- const dltFallbackListingPreviewhandler = (data) => {
1423
- const {
1424
- 'updated-sms-editor': updatedSmsEditor = [],
1425
- 'sms-editor': smsEditor = '',
1426
- } = data.versions.base || {};
1427
- setFallbackPreviewmode(true);
1428
- setDltPreviewData(
1429
- updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
1430
- );
1431
- };
1432
-
1433
- const getDltContentCardList = (content, channel) => {
1434
- const extra = [
1435
- <CapIcon
1436
- type="edit"
1437
- style={{ marginRight: '8px' }}
1438
- onClick={() => rcsDltEditSelectHandler(dltEditData)}
1439
- />,
1440
- <CapDropdown
1441
- overlay={(
1442
- <CapMenu>
1443
- <>
1444
- <CapMenu.Item
1445
- className="ant-dropdown-menu-item"
1446
- onClick={() => setFallbackPreviewmode(true)}
1447
- >
1448
- {formatMessage(globalMessages.preview)}
1449
- </CapMenu.Item>
1450
- <CapMenu.Item
1451
- className="ant-dropdown-menu-item"
1452
- onClick={rcsDltCardDeleteHandler}
1453
- >
1454
- {formatMessage(globalMessages.delete)}
1455
- </CapMenu.Item>
1456
- </>
1457
- </CapMenu>
1458
- )}
1459
- >
1460
- <CapIcon type="more" />
1461
- </CapDropdown>,
1462
- ];
1463
- return {
1464
- title: channel,
1465
- content,
1466
- cardType: channel,
1467
- extra,
1468
- };
1469
- };
1470
-
1471
- const getDltSlideBoxContent = () => {
1472
- const loc = {
1473
- pathname: `/sms/edit`,
1474
- query: { type: EMBEDDED, module: LIBRARY },
1475
- search: '',
1476
- };
1477
- const route = { name: SMS.toLowerCase() };
1478
- let dltHeader = '';
1479
- let dltContent = '';
1480
- if (dltMode === RCS_DLT_MODE.TEMPLATES) {
1481
- dltHeader = (
1482
- <CapHeader
1483
- title={formatMessage(messages.rcsDltTitle)}
1484
- description={formatMessage(messages.rcsDltDesc)}
1485
- />
1486
- );
1487
- dltContent = (
1488
- <Templates
1489
- location={loc}
1490
- route={route}
1491
- smsRegister={smsRegister}
1492
- isFullMode={isFullMode}
1493
- isDltFromRcs
1494
- onSelectTemplate={rcsDltEditSelectHandler}
1495
- handlePeviewTemplate={dltFallbackListingPreviewhandler}
1496
- />
1497
- );
1498
- } else if (dltMode === RCS_DLT_MODE.EDIT) {
1499
- dltHeader = formatMessage(messages.dltEditHeader);
1500
- dltContent = (
1501
- <SmsTraiEdit
1502
- location={loc}
1503
- route={route}
1504
- getDefaultTags="outbound"
1505
- handleClose={closeDltContainerHandler}
1506
- templateData={dltEditData}
1507
- getFormSubscriptionData={rcsDltEditCompletionHandler}
1508
- isFullMode={false}
1509
- selectedOfferDetails={selectedOfferDetails}
1510
- />
1511
- );
1512
- }
1513
- return { dltHeader, dltContent };
1514
- };
1515
-
1516
- const renderFallBackSmsComponent = () => {
1517
- // Completely disable fallback functionality when DLT is disabled
1518
- return null;
1519
-
1520
- // const contentArr = [];
1521
- // const showAddCreativeBtnForDlt = isDltEnabled && !showDltCard;
1522
- // const showCardForDlt = isDltEnabled && showDltCard;
1523
- // const showNonDltFallbackComp = !showAddCreativeBtnForDlt && !showCardForDlt;
1524
- // //pushing common fallback sms headings
1525
- // contentArr.push(
1526
- // <CapRow
1527
- // style={{
1528
- // marginBottom: isDltEnabled ? '20px' : '10px',
1529
- // }}
1530
- // >
1531
- // <CapHeader
1532
- // title={(
1533
- // <CapRow type="flex">
1534
- // <CapHeading type="h4">
1535
- // {formatMessage(messages.fallbackLabel)}
1536
- // </CapHeading>
1537
- // <CapTooltipWithInfo
1538
- // placement="right"
1539
- // infoIconProps={{
1540
- // style: { marginLeft: '4px', marginTop: '3px' },
1541
- // }}
1542
- // title={formatMessage(messages.fallbackToolTip)}
1543
- // />
1544
- // </CapRow>
1545
- // )}
1546
- // description={formatMessage(messages.fallbackDesc)}
1547
- // suffix={
1548
- // isDltEnabled ? null : (
1549
- // <CapButton
1550
- // type="flat"
1551
- // className="fallback-preview-btn"
1552
- // prefix={<CapIcon type="eye" />}
1553
- // style={{ color: CAP_SECONDARY.base }}
1554
- // onClick={() => setFallbackPreviewmode(true)}
1555
- // disabled={fallbackMessage === '' || fallbackMessageError}
1556
- // >
1557
- // {formatMessage(globalMessages.preview)}
1558
- // </CapButton>
1559
- // )
1560
- // }
1561
- // />
1562
- // </CapRow>,
1563
- // );
1564
-
1565
- //dlt is enabled, and dlt content is not yet added, show button to add dlt creative
1566
- // showAddCreativeBtnForDlt
1567
- // && contentArr.push(
1568
- // <CapCard className="rcs-dlt-fallback-card">
1569
- // <CapRow type="flex" justify="center" align="middle">
1570
- // <CapColumn span={10}>
1571
- // <CapImage src={addCreativesIcon} />
1572
- // </CapColumn>
1573
- // <CapColumn span={14}>
1574
- // <CapButton
1575
- // className="add-dlt-btn"
1576
- // type="secondary"
1577
- // onClick={addDltMsgHandler}
1578
- // >
1579
- // {formatMessage(messages.addSmsCreative)}
1580
- // </CapButton>
1581
- // </CapColumn>
1582
- // </CapRow>
1583
- // </CapCard>,
1584
- // );
1585
-
1586
- // //dlt is enabled and dlt content is added, show it in a card
1587
- // showCardForDlt
1588
- // && contentArr.push(
1589
- // <CapCustomCardList
1590
- // cardList={[getDltContentCardList(fallbackMessage, SMS)]}
1591
- // className="rcs-dlt-card"
1592
- // />,
1593
- // fallbackMessageError && (
1594
- // <CapError className="rcs-fallback-len-error">
1595
- // {formatMessage(messages.fallbackMsgLenError)}
1596
- // </CapError>
1597
- // ),
1598
- // );
1599
-
1600
- // //dlt is not enabled, show non dlt text area
1601
- // showNonDltFallbackComp
1602
- // && contentArr.push(
1603
- // <>
1604
- // <CapRow>
1605
- // <CapHeader
1606
- // title={(
1607
- // <CapHeading type="h4">
1608
- // {formatMessage(messages.fallbackTextAreaLabel)}
1609
- // </CapHeading>
1610
- // )}
1611
- // suffix={(
1612
- // <TagList
1613
- // label={formatMessage(globalMessages.addLabels)}
1614
- // onTagSelect={onTagSelectFallback}
1615
- // location={location}
1616
- // tags={tags || []}
1617
- // onContextChange={handleOnTagsContextChange}
1618
- // injectedTags={injectedTags || {}}
1619
- // selectedOfferDetails={selectedOfferDetails}
1620
- // />
1621
- // )}
1622
- // />
1623
- // </CapRow>
1624
- // <div className="rcs_fallback_msg_textarea_wrapper">
1625
- // <TextArea
1626
- // id="rcs_fallback_message_textarea"
1627
- // autosize={{ minRows: 3, maxRows: 5 }}
1628
- // placeholder={formatMessage(messages.fallbackMsgPlaceholder)}
1629
- // onChange={onFallbackMessageChange}
1630
- // errorMessage={fallbackMessageError}
1631
- // value={fallbackMessage || ""}
1632
- // />
1633
- // {!aiContentBotDisabled && (
1634
- // <CapAskAira.ContentGenerationBot
1635
- // text={fallbackMessage || ""}
1636
- // setText={(text) => {
1637
- // onFallbackMessageChange({ target: { value: text } });
1638
- // }}
1639
- // iconPlacement="float-br"
1640
- // rootStyle={{
1641
- // bottom: "0.5rem",
1642
- // right: "0.5rem",
1643
- // position: "absolute",
1644
- // }}
1645
- // />
1646
- // )}
1647
- // </div>
1648
- // <CapRow>{fallbackSmsLength()}</CapRow>
1649
- // </>
1650
- // );
1651
-
1652
- // return <>{contentArr}</>;
1653
- };
2307
+ const renderFallBackSmsComponent = () => (
2308
+ <SmsFallback
2309
+ value={smsFallbackData}
2310
+ onChange={setSmsFallbackData}
2311
+ parentLocation={location}
2312
+ smsRegister={smsRegister}
2313
+ isFullMode={isFullMode}
2314
+ selectedOfferDetails={selectedOfferDetails}
2315
+ channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
2316
+ sectionTitle={
2317
+ smsFallbackData
2318
+ ? formatMessage(messages.fallbackLabel)
2319
+ : formatMessage(messages.smsFallbackOptional)
2320
+ }
2321
+ templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
2322
+ templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
2323
+ /* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
2324
+ showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
2325
+ disableSelectTemplate={isEditFlow}
2326
+ eventContextTags={eventContextTags}
2327
+ onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
2328
+ isRcsEditFlow={isEditFlow}
2329
+ />
2330
+ );
1654
2331
 
1655
2332
  const uploadRcsImage = useCallback((file, type, fileParams, index) => {
1656
2333
  setImageError(null);
1657
- const isRcsThumbnail = index === 1;
2334
+ const isRcsThumbnail = isThumbnailAssetIndex(index);
1658
2335
  actions.uploadRcsAsset(file, type, {
1659
2336
  isRcsThumbnail,
1660
2337
  ...fileParams,
@@ -1692,21 +2369,15 @@ const splitTemplateVarString = (str) => {
1692
2369
  const updateOnRcsImageReUpload = useCallback(() => {
1693
2370
  setUpdateRcsImageSrc('');
1694
2371
  }, []);
1695
-
1696
-
1697
- const uploadRcsVideo = (file, type, fileParams) => {
1698
- setImageError(null);
2372
+ const uploadRcsVideo = (file, type, fileParams, index) => {
1699
2373
  actions.uploadRcsAsset(file, type, {
1700
2374
  ...fileParams,
1701
2375
  type: 'video',
1702
2376
  channel: RCS,
1703
2377
  isGenerateVideoThumbnail: false,
1704
- });
2378
+ }, index);
1705
2379
  };
1706
2380
 
1707
- const updateRcsVideoSrc = (val) => {
1708
- setRcsVideoSrc(val);
1709
- };
1710
2381
  const setUpdateRcsVideoSrc = useCallback((index, val) => {
1711
2382
  setRcsVideoSrc(val);
1712
2383
  setAssetList(val);
@@ -1729,13 +2400,13 @@ const splitTemplateVarString = (str) => {
1729
2400
  updateRcsThumbnailSrc('');
1730
2401
  };
1731
2402
 
1732
- const renderThumbnailComponent = () => {
2403
+ const renderThumbnailComponent = () => {
1733
2404
  const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
1734
2405
  return !isEditFlow && (
1735
2406
  <>
1736
2407
  <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
1737
2408
  <CapImageUpload
1738
- style={{ paddingTop: '20px' }}
2409
+ className="rcs-image-upload-with-top-padding"
1739
2410
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1740
2411
  imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
1741
2412
  imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
@@ -1753,6 +2424,7 @@ const splitTemplateVarString = (str) => {
1753
2424
  channel={RCS}
1754
2425
  channelSpecificStyle={!isFullMode}
1755
2426
  skipDimensionValidation={true}
2427
+ showReUploadButton={!isEditFlow && isFullMode}
1756
2428
  />
1757
2429
  </>
1758
2430
  )
@@ -1778,7 +2450,7 @@ const splitTemplateVarString = (str) => {
1778
2450
  value: dim.type,
1779
2451
  label: `${dim.label}`
1780
2452
  }))}
1781
- style={{ marginBottom: '20px' }}
2453
+ className="rcs-dimension-select--bottom-spacing"
1782
2454
  />
1783
2455
  </>
1784
2456
  )}
@@ -1792,7 +2464,7 @@ const splitTemplateVarString = (str) => {
1792
2464
  </div>
1793
2465
  ) : (
1794
2466
  <CapImageUpload
1795
- style={{ paddingTop: '20px' }}
2467
+ className="rcs-image-upload-with-top-padding"
1796
2468
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1797
2469
  imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
1798
2470
  imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
@@ -1803,7 +2475,7 @@ const splitTemplateVarString = (str) => {
1803
2475
  updateImageSrc={setUpdateRcsImageSrc}
1804
2476
  updateOnReUpload={updateOnRcsImageReUpload}
1805
2477
  index={0}
1806
- className="cap-custom-image-upload"
2478
+ className="cap-custom-image-upload rcs-image-upload--top-spacing"
1807
2479
  key={`rcs-uploaded-image-${selectedDimension}`}
1808
2480
  imageData={rcsData}
1809
2481
  channel={RCS}
@@ -1814,7 +2486,7 @@ const splitTemplateVarString = (str) => {
1814
2486
 
1815
2487
  </>
1816
2488
  );
1817
- }
2489
+ }
1818
2490
 
1819
2491
  const renderVideoComponent = () => {
1820
2492
  const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
@@ -1833,7 +2505,7 @@ const splitTemplateVarString = (str) => {
1833
2505
  value: dim.type,
1834
2506
  label: `${dim.label}`
1835
2507
  }))}
1836
- style={{ marginBottom: '20px' }}
2508
+ className="rcs-dimension-select--bottom-spacing"
1837
2509
  />
1838
2510
  )}
1839
2511
  {(isEditFlow || !isFullMode) ? (
@@ -1891,10 +2563,48 @@ const splitTemplateVarString = (str) => {
1891
2563
  };
1892
2564
 
1893
2565
  const getRcsPreview = () => {
2566
+
2567
+ if (templateType === contentType.carousel) {
2568
+ const cardsForPreview = buildCarouselCardsForPreview(carouselData);
2569
+ const carouselDimKey = getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth);
2570
+ const carouselImgDims =
2571
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
2572
+ const carouselVidDims =
2573
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
2574
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
2575
+ // Debug log for embedded/library mode preview payload (carousel)
2576
+ // eslint-disable-next-line no-console
2577
+ return (
2578
+ <UnifiedPreview
2579
+ channel={RCS}
2580
+ content={{
2581
+ carouselData: cardsForPreview,
2582
+ carouselPreviewDimensions: {
2583
+ imageWidth: carouselImgDims.width,
2584
+ imageHeight: carouselImgDims.height,
2585
+ videoThumbWidth: carouselVidDims.width,
2586
+ videoThumbHeight: carouselVidDims.height,
2587
+ },
2588
+ }}
2589
+ device={ANDROID}
2590
+ showDeviceToggle={false}
2591
+ showHeader={false}
2592
+ formatMessage={formatMessage}
2593
+ />
2594
+ );
2595
+ }
1894
2596
 
1895
2597
  const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
1896
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1897
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
2598
+ const isSlotMappingMode = isEditFlow || !isFullMode;
2599
+ const titleVarCountForResolve = isMediaTypeText
2600
+ ? 0
2601
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
2602
+ const resolvedTitle = isMediaTypeText
2603
+ ? ''
2604
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
2605
+ const resolvedDesc = isSlotMappingMode
2606
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
2607
+ : templateDesc;
1898
2608
  return (
1899
2609
  <UnifiedPreview
1900
2610
  channel={RCS}
@@ -1917,51 +2627,39 @@ const splitTemplateVarString = (str) => {
1917
2627
  );
1918
2628
  };
1919
2629
 
1920
- const getUnmappedDesc = (str, mapping) => {
1921
- if (!str) return '';
1922
- if (!mapping || Object.keys(mapping).length === 0) return str;
1923
- let result = str;
1924
- const replacements = [];
1925
- Object.entries(mapping).forEach(([key, value]) => {
1926
- const raw = (value ?? '').toString();
1927
- if (!raw || raw?.trim?.() === '') return;
1928
- const braced = /^\{\{[\s\S]*\}\}$/.test(raw) ? raw : `{{${raw}}}`;
1929
- replacements.push({ key, needle: raw });
1930
- if (braced !== raw) replacements.push({ key, needle: braced });
1931
- });
1932
- const seen = new Set();
1933
- const uniq = replacements
1934
- .filter(({ key, needle }) => {
1935
- const id = `${key}::${needle}`;
1936
- if (seen.has(id)) return false;
1937
- seen.add(id);
1938
- return true;
1939
- })
1940
- .sort((a, b) => (b.needle.length - a.needle.length));
1941
-
1942
- uniq.forEach(({ key, needle }) => {
1943
- if (!needle) return;
1944
- const escaped = needle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1945
- const regex = new RegExp(escaped, 'g');
1946
- result = result.replace(regex, `{{${key}}}`);
1947
- });
1948
- return result;
1949
- };
1950
-
1951
2630
  const createPayload = () => {
1952
- const base = get(dltEditData, `versions.base`, {});
1953
- const {
1954
- template_id: templateId = '',
1955
- template_name = '',
1956
- 'sms-editor': template = '',
1957
- header: registeredSenderIds = [],
1958
- } = base;
1959
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1960
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
2631
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1961
2632
  const alignment = isMediaTypeImage
1962
2633
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
1963
2634
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
1964
2635
 
2636
+ const heightTypeForCardWidth = isMediaTypeText
2637
+ ? undefined
2638
+ : isMediaTypeImage
2639
+ ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
2640
+ : isMediaTypeVideo
2641
+ ? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
2642
+ : undefined;
2643
+ const cardWidthFromSelection =
2644
+ heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
2645
+
2646
+ /** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
2647
+ const smsFallbackPayloadContent = buildSmsFallBackContentForPayload({
2648
+ smsFallbackData,
2649
+ templateData,
2650
+ isFullMode,
2651
+ smsRegister,
2652
+ });
2653
+
2654
+ const carouselCardContent = isCarouselType
2655
+ ? buildCarouselCardContentForPayload(carouselData, {
2656
+ isSlotMappingMode,
2657
+ cardVarMapped,
2658
+ selectedCarouselHeight,
2659
+ rcsVarRegex,
2660
+ })
2661
+ : null;
2662
+
1965
2663
  const payload = {
1966
2664
  name: templateName,
1967
2665
  versions: {
@@ -1970,16 +2668,22 @@ const splitTemplateVarString = (str) => {
1970
2668
  RCS: {
1971
2669
  rcsContent: {
1972
2670
  ...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
1973
- cardType: STANDALONE,
1974
- cardSettings: {
1975
- cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
1976
- ...(alignment && { mediaAlignment: alignment }),
1977
- cardWidth: SMALL,
1978
- },
1979
- cardContent: [
2671
+ cardType: isCarouselType ? contentType.carousel : STANDALONE,
2672
+ cardSettings: isCarouselType
2673
+ ? { cardOrientation: VERTICAL, cardWidth: selectedCarouselWidth || SMALL }
2674
+ : {
2675
+ cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
2676
+ ...(alignment && { mediaAlignment: alignment }),
2677
+ cardWidth: SMALL,
2678
+ },
2679
+ cardContent: isCarouselType
2680
+ ? carouselCardContent
2681
+ : [
1980
2682
  {
1981
- title: resolvedTitle,
1982
- description: resolvedDesc,
2683
+ // Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
2684
+ // / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
2685
+ title: templateTitle,
2686
+ description: templateDesc,
1983
2687
  mediaType: templateMediaType,
1984
2688
  ...(!isMediaTypeText && {media: {
1985
2689
  mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
@@ -1988,23 +2692,32 @@ const splitTemplateVarString = (str) => {
1988
2692
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
1989
2693
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
1990
2694
  }}),
1991
- ...(!isFullMode && (() => {
1992
- const tokens = [
1993
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
1994
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
2695
+ ...(isSlotMappingMode && (() => {
2696
+ const templateVarTokens = [
2697
+ ...(templateTitle?.match(rcsVarRegex) ?? []),
2698
+ ...(templateDesc?.match(rcsVarRegex) ?? []),
1995
2699
  ];
1996
- const allowedKeys = tokens
1997
- .map((t) => getVarNameFromToken(t))
1998
- .filter(Boolean);
1999
- const nextMap = {};
2000
- allowedKeys.forEach((k) => {
2001
- if (Object.prototype.hasOwnProperty.call(cardVarMapped || {}, k)) {
2002
- nextMap[k] = cardVarMapped[k];
2003
- } else {
2004
- nextMap[k] = '';
2005
- }
2700
+ const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
2701
+ cardVarMapped,
2702
+ );
2703
+ // Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
2704
+ // semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
2705
+ // and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
2706
+ const persistedSlotVarMap = {};
2707
+ templateVarTokens.forEach((token, slotIndexZeroBased) => {
2708
+ const varName = getVarNameFromToken(token);
2709
+ if (!varName) return;
2710
+ const resolvedRawValue = resolveCardVarMappedSlotValue(
2711
+ cardVarMappedForRcsCardOnly,
2712
+ varName,
2713
+ slotIndexZeroBased,
2714
+ isSlotMappingMode,
2715
+ rcsSpanningSemanticVarNames.has(varName),
2716
+ );
2717
+ const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
2718
+ persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
2006
2719
  });
2007
- return { cardVarMapped: nextMap };
2720
+ return { cardVarMapped: persistedSlotVarMap };
2008
2721
  })()),
2009
2722
  ...(suggestions.length > 0 && { suggestions }),
2010
2723
  }
@@ -2012,17 +2725,7 @@ const splitTemplateVarString = (str) => {
2012
2725
  contentType: isFullMode ? templateType : RICHCARD,
2013
2726
  ...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
2014
2727
  },
2015
- smsFallBackContent: {
2016
- message: fallbackMessage,
2017
- ...(isDltEnabled && {
2018
- templateConfigs: {
2019
- templateId,
2020
- templateName: template_name,
2021
- template,
2022
- registeredSenderIds,
2023
- },
2024
- }),
2025
- },
2728
+ ...smsFallbackPayloadContent,
2026
2729
  },
2027
2730
  },
2028
2731
  },
@@ -2032,6 +2735,132 @@ const splitTemplateVarString = (str) => {
2032
2735
  return payload;
2033
2736
  };
2034
2737
 
2738
+ /** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
2739
+ const testPreviewFormData = useMemo(() => {
2740
+ const payload = createPayload();
2741
+ const rcs = payload?.versions?.base?.content?.RCS;
2742
+ if (!rcs) return null;
2743
+ // createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
2744
+ const accountIdForCreateMessageMeta =
2745
+ (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
2746
+ ? String(wecrmAccountId)
2747
+ : accountId;
2748
+ const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
2749
+ let rcsForTest = {
2750
+ ...rcs,
2751
+ rcsContent: {
2752
+ ...rcs.rcsContent,
2753
+ ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
2754
+ },
2755
+ };
2756
+ /** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
2757
+ const cardContent = rcsForTest.rcsContent?.cardContent;
2758
+ if (Array.isArray(cardContent) && cardContent[0]) {
2759
+ if (isCarouselType) {
2760
+ // Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
2761
+ // user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
2762
+ // - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
2763
+ // - prepareTagExtractionPayload can extract tag metadata from the card content.
2764
+ // Track cumulative slotOffset across cards: each card's title/description vars occupy
2765
+ // sequential global slot indices ({{1}},{{2}} in card 0; {{3}},{{4}} in card 1, …).
2766
+ // Without the offset, every card restarts at slotKey="1" and resolves against card 0's
2767
+ // mappings, causing tags from card 1+ to be missing or wrong.
2768
+ let carouselSlotOffset = 0;
2769
+ rcsForTest = {
2770
+ ...rcsForTest,
2771
+ rcsContent: {
2772
+ ...rcsForTest.rcsContent,
2773
+ cardContent: cardContent.map((card) => {
2774
+ const rawTitle = card.title || '';
2775
+ const rawDesc = card.description || '';
2776
+ const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
2777
+ const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
2778
+ const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
2779
+ const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
2780
+ carouselSlotOffset += titleVarCount + descVarCount;
2781
+ return { ...card, title: resolvedTitle, description: resolvedDesc };
2782
+ }),
2783
+ },
2784
+ };
2785
+ } else if (isSlotMappingModeForPreview) {
2786
+ // Standalone card: coalesce cardVarMapped with the template's slot names so the preview
2787
+ // API receives a correctly-keyed var map for non-carousel templates.
2788
+ const fullCardVarMapped = coalesceCardVarMappedToTemplate(
2789
+ pickRcsCardVarMappedEntries(cardVarMapped),
2790
+ templateTitle,
2791
+ templateDesc,
2792
+ rcsVarRegex,
2793
+ );
2794
+ rcsForTest = {
2795
+ ...rcsForTest,
2796
+ rcsContent: {
2797
+ ...rcsForTest.rcsContent,
2798
+ cardContent: [
2799
+ { ...cardContent[0], cardVarMapped: fullCardVarMapped },
2800
+ ...cardContent.slice(1),
2801
+ ],
2802
+ },
2803
+ };
2804
+ }
2805
+ }
2806
+ const out = {
2807
+ versions: {
2808
+ base: {
2809
+ content: {
2810
+ RCS: rcsForTest,
2811
+ },
2812
+ },
2813
+ },
2814
+ };
2815
+ // Use the smsFallbackTemplateConfigs already built by createPayload() / buildSmsFallBackContentForPayload —
2816
+ // it merges the API baseline (templateId, registeredSenderIds, templateName, etc.) with local
2817
+ // smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced smsTemplateId
2818
+ // in campaign mode. This also keeps templateName (and all other fields) in sync with the approval
2819
+ // payload automatically.
2820
+ const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
2821
+ if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
2822
+ out.templateConfigs = {
2823
+ ...smsFallbackTcFromPayload,
2824
+ traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
2825
+ };
2826
+ }
2827
+ return out;
2828
+ }, [
2829
+ templateName,
2830
+ templateTitle,
2831
+ templateDesc,
2832
+ templateMediaType,
2833
+ cardVarMapped,
2834
+ carouselData,
2835
+ suggestions,
2836
+ rcsImageSrc,
2837
+ rcsVideoSrc,
2838
+ rcsThumbnailSrc,
2839
+ selectedDimension,
2840
+ smsFallbackData,
2841
+ isFullMode,
2842
+ isEditFlow,
2843
+ templateType,
2844
+ accountId,
2845
+ wecrmAccountId,
2846
+ accessToken,
2847
+ accountName,
2848
+ hostName,
2849
+ smsRegister,
2850
+ ]);
2851
+
2852
+ /**
2853
+ * Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
2854
+ * with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
2855
+ * miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
2856
+ */
2857
+ const librarySmsFallbackMergedForValidation = useMemo(
2858
+ () => mergeSmsFallbackForLibrary(templateData, smsFallbackData, isFullMode),
2859
+ [isFullMode, templateData, smsFallbackData],
2860
+ );
2861
+
2862
+
2863
+
2035
2864
  const actionCallback = ({ errorMessage, resp }, isEdit) => {
2036
2865
  // eslint-disable-next-line no-undef
2037
2866
  const error = errorMessage?.message || errorMessage;
@@ -2061,6 +2890,9 @@ const splitTemplateVarString = (str) => {
2061
2890
  _id: params?.id,
2062
2891
  validity: true,
2063
2892
  type: RCS,
2893
+ // CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
2894
+ // the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
2895
+ closeSlideBoxAfterSubmit: !isFullMode,
2064
2896
  };
2065
2897
  getFormData(formDataParams);
2066
2898
  };
@@ -2074,6 +2906,7 @@ const splitTemplateVarString = (str) => {
2074
2906
  actionCallback({ resp, errorMessage });
2075
2907
  setSpin(false); // Always turn off spinner
2076
2908
  if (!errorMessage) {
2909
+ setTemplateStatus(RCS_STATUSES.pending);
2077
2910
  onCreateComplete();
2078
2911
  }
2079
2912
  });
@@ -2085,6 +2918,81 @@ const splitTemplateVarString = (str) => {
2085
2918
  }
2086
2919
  };
2087
2920
 
2921
+ const smsFallbackBlocksDone = () => checkSmsFallbackBlocksDone({
2922
+ isFullMode,
2923
+ smsRegister,
2924
+ smsFallbackData,
2925
+ templateData,
2926
+ smsFallbackMergedForValidation: librarySmsFallbackMergedForValidation,
2927
+ });
2928
+
2929
+ /**
2930
+ * Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
2931
+ * semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
2932
+ * / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
2933
+ */
2934
+ const isLibraryCampaignCardVarMappingIncomplete = () => {
2935
+ if (isFullMode) return false;
2936
+ const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
2937
+ rcsVarTestRegex.test(elem),
2938
+ );
2939
+ const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
2940
+ rcsVarTestRegex.test(elem),
2941
+ );
2942
+ const orderedVarNames = [
2943
+ ...titleTokens.map((token) => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
2944
+ ...descTokens.map((token) => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
2945
+ ];
2946
+ if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
2947
+ return true;
2948
+ }
2949
+ return orderedVarNames.some((name, globalIdx) => {
2950
+ const slotValue = resolveCardVarMappedSlotValue(
2951
+ cardVarMapped,
2952
+ name,
2953
+ globalIdx,
2954
+ true,
2955
+ rcsSpanningSemanticVarNames.has(name),
2956
+ );
2957
+ const stringValue = slotValue == null ? '' : String(slotValue);
2958
+ return stringValue.trim() === '';
2959
+ });
2960
+ };
2961
+
2962
+ const isCarouselLibraryIncomplete = () => {
2963
+ if (!isCarouselType || isFullMode) return false;
2964
+ if ((carouselErrors || []).some((err) => err?.title || err?.description)) return true;
2965
+ // Block if any card's title or description has no content at all (no static text and no variable tokens).
2966
+ const hasEmptyField = (carouselData || []).some((card) =>
2967
+ ['title', 'description'].some((field) => !(card?.[field] || '').trim())
2968
+ );
2969
+ if (hasEmptyField) return true;
2970
+ const unfilledVar = (carouselData || []).some((card) =>
2971
+ ['title', 'description'].some((field) => {
2972
+ const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
2973
+ return tokens.some((token) => {
2974
+ const name = token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
2975
+ const slotValue = cardVarMapped?.[name];
2976
+ return slotValue == null || String(slotValue).trim() === '';
2977
+ });
2978
+ })
2979
+ );
2980
+ return unfilledVar;
2981
+ };
2982
+
2983
+ // Mirror WhatsApp's isEditDoneDisabled: block Done while a CTA button's dynamic URL still
2984
+ // holds the unresolved {{1}} placeholder (no real personalization tag assigned yet).
2985
+ const hasUnresolvedDynamicCtaUrl = () =>
2986
+ isCarouselType
2987
+ ? (carouselData || []).some((card) =>
2988
+ (card?.suggestions || []).some((suggestion) =>
2989
+ suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
2990
+ )
2991
+ )
2992
+ : suggestions.some((suggestion) =>
2993
+ suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
2994
+ );
2995
+
2088
2996
  const isDisableDone = () => {
2089
2997
  if(isEditFlow){
2090
2998
  return false;
@@ -2095,46 +3003,26 @@ const splitTemplateVarString = (str) => {
2095
3003
  }
2096
3004
  }
2097
3005
 
2098
- if(!isFullMode){
2099
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2100
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2101
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2102
-
2103
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2104
- return true;
2105
- }
2106
-
2107
- const hasEmptyMapping =
2108
- cardVarMapped &&
2109
- Object.keys(cardVarMapped).length > 0 &&
2110
- Object.entries(cardVarMapped).some(([_, v]) => {
2111
- if (typeof v !== 'string') return !v; // null/undefined
2112
- return v.trim() === ''; // empty string
2113
- });
2114
-
2115
- if (hasEmptyMapping) {
2116
- return true;
2117
- }
3006
+ if (isCarouselLibraryIncomplete()) {
3007
+ return true;
3008
+ }
2118
3009
 
2119
- const anyMissing = allVars.some(name => {
2120
- const v = cardVarMapped?.[name];
2121
- if (typeof v !== 'string') return !v;
2122
- return v.trim() === '';
2123
- });
2124
- if (anyMissing) {
2125
- return true;
2126
- }
3010
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
3011
+ return true;
2127
3012
  }
2128
3013
 
2129
- if (isMediaTypeText && templateDesc.trim() === '') {
3014
+ if (smsFallbackBlocksDone()) {
2130
3015
  return true;
3016
+ }
2131
3017
 
3018
+ if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
3019
+ return true;
2132
3020
  }
2133
- if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
3021
+ if (!isCarouselType && isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
2134
3022
  return true;
2135
3023
  }
2136
3024
 
2137
- if (isMediaTypeVideo && (rcsVideoSrc.videoSrc === '' || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
3025
+ if (!isCarouselType && isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
2138
3026
  return true;
2139
3027
  }
2140
3028
  if (buttonType.includes(CTA)) {
@@ -2146,72 +3034,60 @@ const splitTemplateVarString = (str) => {
2146
3034
  return true;
2147
3035
  }
2148
3036
  }
2149
- if (templateDescError || templateTitleError || fallbackMessageError) {
3037
+ if (hasUnresolvedDynamicCtaUrl()) {
3038
+ return true;
3039
+ }
3040
+ if (templateDescError || templateTitleError) {
3041
+ return true;
3042
+ }
3043
+ if (isSmsFallbackContentTooLong(smsFallbackData, FALLBACK_MESSAGE_MAX_LENGTH)) {
2150
3044
  return true;
2151
3045
  }
2152
3046
  return false;
2153
3047
  };
2154
3048
 
2155
3049
  const isEditDisableDone = () => {
2156
-
2157
- if (templateStatus !== RCS_STATUSES.approved) {
3050
+ if (isFullMode && !isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
2158
3051
  return true;
2159
3052
  }
2160
3053
 
2161
- if (!isFullMode) {
2162
- if (templateName.trim() === '' || templateNameError) {
2163
- return true;
2164
- }
3054
+ if (isCarouselLibraryIncomplete()) {
3055
+ return true;
2165
3056
  }
2166
- if(!isFullMode){
2167
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2168
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2169
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2170
3057
 
2171
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2172
- return true;
2173
- }
2174
-
2175
- const hasEmptyMapping =
2176
- cardVarMapped &&
2177
- Object.keys(cardVarMapped).length > 0 &&
2178
- Object.entries(cardVarMapped).some(([_, v]) => {
2179
- if (typeof v !== 'string') return !v; // null/undefined
2180
- return v.trim() === ''; // empty string
2181
- });
2182
-
2183
- if (hasEmptyMapping) {
2184
- return true;
2185
- }
3058
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
3059
+ return true;
3060
+ }
2186
3061
 
2187
- const anyMissing = allVars.some(name => {
2188
- const v = cardVarMapped?.[name];
2189
- if (typeof v !== 'string') return !v;
2190
- return v.trim() === '';
2191
- });
2192
- if (anyMissing) {
2193
- return true;
2194
- }
3062
+ if (smsFallbackBlocksDone()) {
3063
+ return true;
2195
3064
  }
2196
- if (isMediaTypeText && templateDesc.trim() === '') {
3065
+
3066
+ if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
2197
3067
  return true;
2198
3068
  }
2199
- if (isMediaTypeImage && rcsImageSrc === '') {
3069
+ if (!isCarouselType && isMediaTypeImage && rcsImageSrc === '') {
2200
3070
  return true;
2201
3071
  }
2202
- if(isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
3072
+ if (!isCarouselType && isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
2203
3073
  return true;
2204
3074
  }
2205
3075
 
2206
3076
  if (buttonType.includes(CTA)) {
2207
- const hasValidButtons = suggestions.every(suggestion =>
3077
+ const hasValidButtons = suggestions.every(suggestion =>
2208
3078
  suggestion.text && suggestion.url && !suggestionError && !forbiddenCharactersValidation(suggestion.text)
2209
3079
  );
2210
3080
  if (!hasValidButtons) {
2211
3081
  return true;
2212
3082
  }
2213
3083
  }
2214
- if (templateTitleError || templateDescError || fallbackMessageError) {
3084
+ if (hasUnresolvedDynamicCtaUrl()) {
3085
+ return true;
3086
+ }
3087
+ if (templateTitleError || templateDescError) {
3088
+ return true;
3089
+ }
3090
+ if (isSmsFallbackContentTooLong(smsFallbackData, FALLBACK_MESSAGE_MAX_LENGTH)) {
2215
3091
  return true;
2216
3092
  }
2217
3093
  return false;
@@ -2261,85 +3137,108 @@ const splitTemplateVarString = (str) => {
2261
3137
  };
2262
3138
 
2263
3139
  const getMainContent = () => {
2264
- if (showDltContainer && !fallbackPreviewmode) {
2265
- const dltSlideBoxContent = showDltContainer && getDltSlideBoxContent();
2266
- const { dltHeader = '', dltContent = '' } = dltSlideBoxContent;
2267
- return (
2268
- <CapSlideBox
2269
- show={showDltContainer}
2270
- header={dltHeader}
2271
- content={dltContent}
2272
- handleClose={closeDltContainerHandler}
2273
- size="size-xl"
2274
- />
2275
- );
2276
- }
2277
-
2278
3140
  return (
2279
3141
  <>
2280
- {templateStatus !== '' && (<CapRow useLegacy className="template-status-container">
2281
- <CapLabel type="label2">
2282
- {formatMessage(messages.templateStatusLabel)}
2283
- </CapLabel>
2284
-
2285
- {templateStatus && (
2286
- <CapAlert
2287
- message={getTemplateStatusMessage()}
2288
- type={getTemplateStatusType(templateStatus)}
2289
- />
2290
- )}
2291
- </CapRow>
3142
+ {templateStatus !== '' && (
3143
+ <CapRow useLegacy className="template-status-container">
3144
+ <CapColumn span={14}>
3145
+ <CapLabel type="label2">
3146
+ {formatMessage(messages.templateStatusLabel)}
3147
+ </CapLabel>
3148
+
3149
+ {!isHostInfoBip && templateStatus && (
3150
+ <CapAlert
3151
+ message={getTemplateStatusMessage()}
3152
+ type={getTemplateStatusType(templateStatus)}
3153
+ />
3154
+ )}
3155
+ </CapColumn>
3156
+ </CapRow>
2292
3157
  )}
2293
- <CapRow className="cap-rcs-creatives">
3158
+ <CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
2294
3159
  <CapColumn span={14}>
2295
3160
  {/* template name */}
2296
3161
  {isFullMode && (
2297
- <CapInput
2298
- id="rcs_template_name_input"
2299
- data-testid="template_name"
2300
- onChange={onTemplateNameChange}
2301
- errorMessage={templateNameError}
2302
- placeholder={formatMessage(
2303
- globalMessages.templateNamePlaceholder,
2304
- )}
2305
- value={templateName || ''}
2306
- size="default"
2307
- label={formatMessage(globalMessages.creativeNameLabel)}
2308
- disabled={(isEditFlow || !isFullMode)}
2309
- />
3162
+ isEditFlow ? (
3163
+ <div className="rcs-creative-name-readonly">
3164
+ <CapHeading type="h4">
3165
+ {formatMessage(globalMessages.creativeNameLabel)}
3166
+ </CapHeading>
3167
+ <CapHeading type="h5" className="rcs-creative-name-value">
3168
+ {templateName || '-'}
3169
+ </CapHeading>
3170
+ </div>
3171
+ ) : (
3172
+ <CapInput
3173
+ id="rcs_template_name_input"
3174
+ data-testid="template_name"
3175
+ onChange={onTemplateNameChange}
3176
+ errorMessage={templateNameError}
3177
+ placeholder={formatMessage(
3178
+ globalMessages.templateNamePlaceholder,
3179
+ )}
3180
+ value={templateName || ''}
3181
+ size="default"
3182
+ label={formatMessage(globalMessages.creativeNameLabel)}
3183
+ disabled={(isEditFlow || !isFullMode)}
3184
+ />
3185
+ )
3186
+ )}
3187
+ {isEditFlow ? (
3188
+ <div className="rcs-creative-name-readonly">
3189
+ <CapHeading type="h4">
3190
+ {formatMessage(messages.templateTypeLabel)}
3191
+ </CapHeading>
3192
+ <CapHeading type="h5" className="rcs-creative-name-value">
3193
+ {TEMPLATE_TYPE_OPTIONS.find((option) => option.value === templateType)?.label || '-'}
3194
+ </CapHeading>
3195
+ </div>
3196
+ ) : (
3197
+ <>
3198
+ {renderLabel('templateTypeLabel')}
3199
+ <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
3200
+ <CapRadioGroup
3201
+ id="select-rcs-template-type"
3202
+ options={TEMPLATE_TYPE_OPTIONS}
3203
+ onChange={onTemplateTypeChange}
3204
+ value={templateType}
3205
+ disabled={!isFullMode}
3206
+ />
3207
+ </ConfigProvider>
3208
+ </>
2310
3209
  )}
2311
- {renderLabel('templateTypeLabel')}
2312
- <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
2313
- <CapRadioGroup
2314
- id="select-rcs-template-type"
2315
- options={TEMPLATE_TYPE_OPTIONS}
2316
- onChange={onTemplateTypeChange}
2317
- value={templateType}
2318
- disabled={(isEditFlow || !isFullMode)}
2319
- />
2320
- </ConfigProvider>
2321
3210
 
2322
- {/* Show media only for rich_card or carousel */}
2323
- {(templateType === contentType.rich_card || templateType === contentType.carousel) && (
3211
+ {templateType === contentType.carousel ? (
3212
+ renderCarouselSection()
3213
+ ) : (
2324
3214
  <>
2325
- {renderLabel('mediaLabel')}
2326
- <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
3215
+ {/* Show media only for rich_card */}
3216
+ {templateType === contentType.rich_card && (
3217
+ <>
3218
+ {renderLabel('mediaLabel')}
3219
+ <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
2327
3220
  <CapRadioGroup
2328
- options={mediaRadioOptions || []}
2329
- value={templateMediaType}
2330
- onChange={onTemplateMediaTypeChange}
2331
- disabled={(isEditFlow || !isFullMode)}
2332
- className="rcs-radio"
2333
- />
2334
- </ConfigProvider>
3221
+ options={mediaRadioOptions || []}
3222
+ value={templateMediaType}
3223
+ onChange={onTemplateMediaTypeChange}
3224
+ disabled={(isEditFlow || !isFullMode)}
3225
+ className="rcs-radio"
3226
+ />
3227
+ </ConfigProvider>
2335
3228
  <div className="rcs-container-image">
2336
- {getMediaBasedComponent()}
2337
- </div>
3229
+ {getMediaBasedComponent()}
3230
+ </div>
3231
+ </>
3232
+ )}
3233
+ {renderTextComponent()}
3234
+ </>
3235
+ )}
3236
+ {(!isEditFlow || hasMeaningfulSmsFallbackShape(smsFallbackData)) && (
3237
+ <>
3238
+ <CapDivider className="rcs-fallback-section-divider" />
3239
+ {renderFallBackSmsComponent()}
2338
3240
  </>
2339
3241
  )}
2340
- {renderTextComponent()}
2341
- <CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
2342
- {renderFallBackSmsComponent()}
2343
3242
  <div className="rcs-scroll-div" />
2344
3243
  </CapColumn>
2345
3244
  <CapColumn span={8} offset={2}>
@@ -2350,7 +3249,8 @@ const splitTemplateVarString = (str) => {
2350
3249
 
2351
3250
 
2352
3251
  <div className="rcs-footer">
2353
- {!isEditFlow && (
3252
+ {/* 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). */}
3253
+ {!isEditFlow && isFullMode && (
2354
3254
  <>
2355
3255
  <div className="button-disabled-tooltip-wrapper">
2356
3256
  <CapButton
@@ -2358,7 +3258,9 @@ const splitTemplateVarString = (str) => {
2358
3258
  disabled={isDisableDone()}
2359
3259
  className="rcs-done-btn"
2360
3260
  >
2361
- <FormattedMessage {...messages.sendForApprovalButtonLabel} />
3261
+ <FormattedMessage
3262
+ {...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
3263
+ />
2362
3264
  </CapButton>
2363
3265
  </div>
2364
3266
  <CapTooltip
@@ -2371,7 +3273,6 @@ const splitTemplateVarString = (str) => {
2371
3273
  className="rcs-test-preview-btn"
2372
3274
  type="secondary"
2373
3275
  disabled={true}
2374
- style={{ marginLeft: "8px" }}
2375
3276
  >
2376
3277
  <FormattedMessage {...creativesMessages.testAndPreview} />
2377
3278
  </CapButton>
@@ -2392,7 +3293,7 @@ const splitTemplateVarString = (str) => {
2392
3293
  </div>
2393
3294
  </>
2394
3295
  )}
2395
- {isEditFlow && templateStatus === RCS_STATUSES.approved && (
3296
+ {isEditFlow && (isHostInfoBip || templateStatus === RCS_STATUSES.approved) && (
2396
3297
  <>
2397
3298
  <CapButton
2398
3299
  onClick={handleTestAndPreview}
@@ -2404,51 +3305,6 @@ const splitTemplateVarString = (str) => {
2404
3305
  </>
2405
3306
  )}
2406
3307
  </div>
2407
-
2408
-
2409
- {fallbackPreviewmode && (
2410
- <CapSlideBox
2411
- className="rcs-fallback-preview"
2412
- show={fallbackPreviewmode}
2413
- header={(
2414
- <CapHeading type="h7" style={{ color: CAP_G01 }}>
2415
- {formatMessage(messages.fallbackPreviewtitle)}
2416
- </CapHeading>
2417
- )}
2418
- content={(
2419
- <>
2420
- <UnifiedPreview
2421
- channel={RCS}
2422
- content={{
2423
- rcsPreviewContent: {
2424
- rcsDesc: tempMsg,
2425
- },
2426
- }}
2427
- device={ANDROID}
2428
- showDeviceToggle={false}
2429
- showHeader={false}
2430
- formatMessage={formatMessage}
2431
- />
2432
- <CapHeading
2433
- type="h3"
2434
- style={{ textAlign: 'center', width: '100%' }}
2435
- className="margin-t-16"
2436
- >
2437
- {formatMessage(messages.totalCharacters, {
2438
- smsCount: Math.ceil(
2439
- tempMsg?.length / FALLBACK_MESSAGE_MAX_LENGTH,
2440
- ),
2441
- number: tempMsg.length,
2442
- })}
2443
- </CapHeading>
2444
- </>
2445
- )}
2446
- handleClose={() => {
2447
- setFallbackPreviewmode(false);
2448
- setDltPreviewData('');
2449
- }}
2450
- />
2451
- )}
2452
3308
  </>
2453
3309
  );
2454
3310
  };
@@ -2457,29 +3313,49 @@ const splitTemplateVarString = (str) => {
2457
3313
  <CapSpin spinning={loadingTags || spin}>
2458
3314
  {getMainContent()}
2459
3315
  </CapSpin>
3316
+
2460
3317
  <TestAndPreviewSlidebox
2461
3318
  show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
2462
3319
  onClose={handleCloseTestAndPreview}
2463
- formData={null} // RCS doesn't use formData structure like SMS
2464
- content={getTemplateContent()}
3320
+ formData={testPreviewFormData}
3321
+ content={testAndPreviewContent}
2465
3322
  currentChannel={RCS}
3323
+ orgUnitId={orgUnitId}
3324
+ rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
3325
+ smsFallbackContent={
3326
+ smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
3327
+ ? {
3328
+ templateContent:
3329
+ smsFallbackData.templateContent || smsFallbackData.content || '',
3330
+ templateName: smsFallbackData.templateName || '',
3331
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
3332
+ ? mergeRcsSmsFallbackVarMapLayers(
3333
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
3334
+ smsFallbackData,
3335
+ )
3336
+ : mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
3337
+ }
3338
+ : null
3339
+ }
3340
+ smsRegister={smsRegister}
2466
3341
  />
2467
3342
  </>
2468
3343
  );
2469
3344
  };
2470
3345
 
3346
+
2471
3347
  const mapStateToProps = createStructuredSelector({
2472
3348
  rcsData: makeSelectRcs(),
2473
3349
  accountData: makeSelectAccount(),
2474
3350
  metaEntities: makeSelectMetaEntities(),
2475
3351
  loadingTags: isLoadingMetaEntities(),
2476
3352
  injectedTags: setInjectedTags(),
3353
+ currentOrgDetails: selectCurrentOrgDetails(),
2477
3354
  });
2478
3355
 
2479
3356
  const mapDispatchToProps = (dispatch) => ({
2480
3357
  actions: bindActionCreators(RcsActions, dispatch),
2481
3358
  globalActions: bindActionCreators(globalActions, dispatch),
2482
- templatesActions: bindActionCreators(TemplatesActions, dispatch),
2483
3359
  });
2484
3360
 
2485
3361
  const withReducer = injectReducer({ key: 'rcs', reducer: v2RcsReducer });