@capillarytech/creatives-library 9.0.28 → 9.0.29-alpha.2

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 (149) 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 +99 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +243 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +20 -1
  11. package/v2Components/CapActionButton/index.js +193 -144
  12. package/v2Components/CapActionButton/index.scss +232 -14
  13. package/v2Components/CapActionButton/messages.js +19 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  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 +328 -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/Email/index.js +3 -63
  96. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/carouselUtils.js +256 -0
  99. package/v2Containers/Rcs/components/CarouselCard.js +315 -0
  100. package/v2Containers/Rcs/components/CarouselCardButtons.js +93 -0
  101. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  102. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  103. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  104. package/v2Containers/Rcs/constants.js +137 -16
  105. package/v2Containers/Rcs/index.js +1912 -958
  106. package/v2Containers/Rcs/index.scss +443 -8
  107. package/v2Containers/Rcs/messages.js +45 -22
  108. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +522 -0
  109. package/v2Containers/Rcs/tests/CarouselCard.test.js +465 -0
  110. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  111. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  112. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  113. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  114. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  115. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  116. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  117. package/v2Containers/Rcs/tests/index.test.js +400 -40
  118. package/v2Containers/Rcs/tests/mockData.js +38 -0
  119. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
  120. package/v2Containers/Rcs/tests/utils.test.js +762 -30
  121. package/v2Containers/Rcs/utils.js +571 -12
  122. package/v2Containers/Sms/Create/index.js +115 -48
  123. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  124. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  125. package/v2Containers/SmsTrai/Create/index.js +9 -4
  126. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  127. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  128. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  129. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  130. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  131. package/v2Containers/SmsWrapper/index.js +37 -8
  132. package/v2Containers/TagList/index.js +5 -0
  133. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  134. package/v2Containers/Templates/_templates.scss +196 -12
  135. package/v2Containers/Templates/actions.js +11 -0
  136. package/v2Containers/Templates/constants.js +2 -0
  137. package/v2Containers/Templates/index.js +131 -59
  138. package/v2Containers/Templates/sagas.js +57 -13
  139. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  140. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  141. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  142. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  143. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  144. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  145. package/v2Containers/TemplatesV2/index.js +88 -25
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  147. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  148. package/v2Containers/Whatsapp/index.js +3 -20
  149. 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,67 @@ 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
+ computeCarouselVarSlotKeys,
179
+ } from './carouselUtils';
180
+ import CarouselDimensionSelection from './components/CarouselDimensionSelection';
181
+ import CarouselCard from './components/CarouselCard';
125
182
 
126
183
 
127
184
  const { Group: CapCheckboxGroup } = CapCheckbox;
@@ -135,31 +192,48 @@ export const Rcs = (props) => {
135
192
  templateData = {},
136
193
  rcsData = {},
137
194
  thumbnailData = {}, // Add separate thumbnail data
138
- templatesActions,
139
195
  globalActions,
140
196
  location,
141
- handleClose,
142
197
  getDefaultTags,
143
198
  supportedTags,
144
199
  metaEntities,
145
200
  injectedTags,
146
201
  loadingTags,
147
202
  getFormData,
148
- isDltEnabled,
149
203
  smsRegister,
204
+ orgUnitId,
150
205
  selectedOfferDetails,
151
206
  eventContextTags,
152
- waitEventContextTags,
153
207
  accountData = {},
208
+ currentOrgDetails,
154
209
  // TestAndPreviewSlidebox props
155
210
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
156
211
  handleTestAndPreview: propsHandleTestAndPreview,
157
212
  handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
213
+ handleClose,
158
214
  } = props || {};
159
215
  const { formatMessage } = intl;
160
216
  const { TextArea } = CapInput;
161
217
  const { CapCustomCardList } = CapCustomCard;
218
+
219
+ // Defensive: React cannot render plain objects as children (crashes with
220
+ // "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
221
+ // can accidentally set an error state to an object (e.g. `{}`).
222
+ const normalizeErrorMessage = (err) => {
223
+ if (!err) return '';
224
+ if (React.isValidElement(err)) return err;
225
+ if (typeof err === JS_TYPES.STRING) return err;
226
+ if (typeof err === JS_TYPES.NUMBER) return String(err);
227
+ if (typeof err === JS_TYPES.OBJECT && typeof err.message === JS_TYPES.STRING) return err.message;
228
+ try {
229
+ return JSON.stringify(err);
230
+ } catch (e) {
231
+ return String(err);
232
+ }
233
+ };
234
+
162
235
  const [isEditFlow, setEditFlow] = useState(false);
236
+ const isEditLike = isEditFlow || !isFullMode;
163
237
  const [tags, updateTags] = useState([]);
164
238
  const [spin, setSpin] = useState(false);
165
239
  //template
@@ -168,33 +242,17 @@ export const Rcs = (props) => {
168
242
  const [templateMediaType, setTemplateMediaType] = useState(
169
243
  RCS_MEDIA_TYPES.NONE,
170
244
  );
171
- const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
172
245
  const [templateTitle, setTemplateTitle] = useState('');
173
246
  const [templateDesc, setTemplateDesc] = useState('');
174
247
  const [templateDescError, setTemplateDescError] = useState(false);
175
248
  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('');
249
+ /** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
250
+ const [smsFallbackData, setSmsFallbackData] = useState(undefined);
187
251
  const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
188
- const [buttonType, setButtonType] = useState(RCS_BUTTON_TYPES.NONE);
252
+ const buttonType = RCS_BUTTON_TYPES.NONE;
189
253
  const [suggestionError, setSuggestionError] = useState(true);
254
+ const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
190
255
  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
256
  const [titleVarMappedData, setTitleVarMappedData] = useState({});
199
257
  const [descVarMappedData, setDescVarMappedData] = useState({});
200
258
  const [titleTextAreaId, setTitleTextAreaId] = useState();
@@ -205,75 +263,63 @@ export const Rcs = (props) => {
205
263
  const [rcsVideoSrc, setRcsVideoSrc] = useState({});
206
264
  const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
207
265
  const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
208
- const [imageError, setImageError] = useState(null);
266
+ // Carousel (UI-only) state
267
+ const [selectedCarousel, setSelectedCarousel] = useState('');
268
+ const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
269
+ const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
270
+ const [carouselData, setCarouselData] = useState([]);
271
+ const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
272
+ const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
273
+ const [carouselResetNonce, setCarouselResetNonce] = useState(0);
274
+ // Title and description each track their own last-focused var slot (mirrors titleTextAreaId/
275
+ // descTextAreaId for the single-card flow) — a single shared id would let a click on the
276
+ // "Add Labels" picker next to one field silently apply to whichever field was focused last,
277
+ // if the user hadn't just clicked into that field's own var slot.
278
+ const [carouselTitleFocusedVarId, setCarouselTitleFocusedVarId] = useState('');
279
+ const [carouselDescFocusedVarId, setCarouselDescFocusedVarId] = useState('');
280
+ const [imageError, setImageError] = useState(null);
209
281
  const [templateTitleError, setTemplateTitleError] = useState(false);
210
282
  const [cardVarMapped, setCardVarMapped] = useState({});
283
+ /** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
284
+ const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
285
+ const lastHydratedRcsCardVarSignatureRef = useRef(null);
286
+
287
+ /**
288
+ * Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
289
+ * without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
290
+ * fallback card / content stopped appearing until re-selected.
291
+ */
292
+ const rcsHydrationDetails = useMemo(
293
+ () => (isFullMode ? rcsData?.templateDetails : templateData),
294
+ [isFullMode, rcsData?.templateDetails, templateData],
295
+ );
211
296
 
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;
297
+ /** 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'). */
298
+ const lastTagSchemaQueryKeyRef = useRef(null);
299
+ /**
300
+ * Library: parent often passes a new `templateData` object reference every render. Re-applying the same
301
+ * SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
302
+ */
303
+ const lastSmsFallbackHydrationKeyRef = useRef(null);
304
+
305
+ const fetchTagSchemaIfNewQuery = useCallback(
306
+ (query) => {
307
+ const key = JSON.stringify(query);
308
+ if (lastTagSchemaQueryKeyRef.current === key) {
309
+ return;
243
310
  }
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
- };
311
+ lastTagSchemaQueryKeyRef.current = key;
312
+ globalActions.fetchSchemaForEntity(query);
313
+ },
314
+ [globalActions],
315
+ );
260
316
 
261
- return contentObj;
262
- }, [
263
- templateMediaType,
264
- templateTitle,
265
- templateDesc,
266
- rcsImageSrc,
267
- rcsVideoSrc,
268
- rcsThumbnailSrc,
269
- suggestions,
270
- selectedDimension,
271
- ]);
317
+ // TestAndPreviewSlidebox state
318
+ const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
272
319
 
273
320
  // Handle Test and Preview button click
274
321
  const handleTestAndPreview = useCallback(() => {
275
322
  setShowTestAndPreviewSlidebox(true);
276
- setIsTestAndPreviewMode(true);
277
323
  if (propsHandleTestAndPreview) {
278
324
  propsHandleTestAndPreview();
279
325
  }
@@ -297,31 +343,334 @@ export const Rcs = (props) => {
297
343
  // For video
298
344
  return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
299
345
  };
346
+ /** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
347
+ const handleSmsFallbackEditorStateChange = useCallback((patch) => {
348
+ setSmsFallbackData((prev) => {
349
+ if (!patch || typeof patch !== 'object') return prev;
350
+ // Bail out when no template has been selected yet — SmsTraiEdit fires
351
+ // onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
352
+ // which would create a non-null smsFallbackData from nothing and cause
353
+ // the card to appear as "Untitled creative" before any save.
354
+ if (!prev) return prev;
355
+ return { ...prev, ...patch };
356
+ });
357
+ }, []);
300
358
 
359
+ /** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
301
360
  const [accountId, setAccountId] = useState('');
361
+ /** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
362
+ const [wecrmAccountId, setWecrmAccountId] = useState('');
302
363
  const [accessToken, setAccessToken] = useState('');
303
364
  const [hostName, setHostName] = useState('');
304
365
  const [accountName, setAccountName] = useState('');
366
+ const isHostInfoBip = hostName === HOST_INFOBIP;
367
+ const isHostIcs = hostName === HOST_ICS;
368
+
369
+ useEffect(() => {
370
+ setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
371
+ }, [isHostIcs]);
305
372
  const [rcsAccount, setRcsAccount] = useState('');
306
373
  useEffect(() => {
307
374
  const accountObj = accountData.selectedRcsAccount || {};
308
375
  if (!isEmpty(accountObj)) {
309
376
  const {
377
+ id: wecrmId,
310
378
  sourceAccountIdentifier = '',
311
379
  configs = {},
312
380
  } = accountObj;
313
-
314
381
  setAccountId(sourceAccountIdentifier);
382
+ setWecrmAccountId(
383
+ wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
384
+ );
315
385
  setAccessToken(configs.accessToken || '');
316
386
  setHostName(accountObj.hostName || '');
317
387
  setAccountName(accountObj.name || '');
318
388
  setRcsAccount(accountObj.id || '');
389
+ } else {
390
+ setAccountId('');
391
+ setWecrmAccountId('');
392
+ setAccessToken('');
393
+ setHostName('');
394
+ setAccountName('');
319
395
  }
320
396
  }, [accountData.selectedRcsAccount]);
321
397
 
322
398
  const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
323
399
  const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
324
400
  const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
401
+ const isCarouselType = templateType === contentType.carousel;
402
+
403
+ const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
404
+ setCarouselData((prev = []) => {
405
+ const updated = cloneDeep(prev);
406
+ if (!updated?.[cardIndex]) return updated;
407
+ if (clearImage) updated[cardIndex].imageSrc = '';
408
+ if (clearVideo) updated[cardIndex].videoAsset = {};
409
+ if (clearThumb) updated[cardIndex].thumbnailSrc = '';
410
+ return updated;
411
+ });
412
+
413
+ if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
414
+ if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
415
+ if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
416
+ };
417
+
418
+ const resetCarouselMediaForAllCards = () => {
419
+ setCarouselData((prev = []) => {
420
+ const updated = cloneDeep(prev);
421
+ updated.forEach((card) => {
422
+ if (!card) return;
423
+ card.imageSrc = '';
424
+ card.videoAsset = {};
425
+ card.thumbnailSrc = '';
426
+ });
427
+ return updated;
428
+ });
429
+ (carouselData || []).forEach((_, idx) => {
430
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
431
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
432
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
433
+ });
434
+ // Force tab panes to remount after global reset (some tab implementations cache inactive panes)
435
+ setCarouselResetNonce((count) => count + 1);
436
+ };
437
+
438
+ // Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
439
+ // updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
440
+ // suggestions typed since the last commit (stale closure).
441
+ const handleCarouselValueChange = (carouselIndex, fields) => {
442
+ setCarouselData((prev = []) => {
443
+ const updated = cloneDeep(prev);
444
+ if (!updated[carouselIndex]) return prev;
445
+ fields.forEach(({ fieldName, value }) => {
446
+ updated[carouselIndex][fieldName] = value;
447
+ });
448
+ return updated;
449
+ });
450
+ };
451
+
452
+ const updateCarouselErrors = (carouselIndex, patch) => {
453
+ setCarouselErrors((prev = []) => {
454
+ const next = Array.isArray(prev) ? [...prev] : [];
455
+ next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
456
+ return next;
457
+ });
458
+ };
459
+
460
+ const deleteCarouselCard = (index) => {
461
+ let nextIdx = 0;
462
+ flushSync(() => {
463
+ setCarouselData((prev = []) => {
464
+ const updated = cloneDeep(prev);
465
+ if (index < 0 || index >= updated.length) return updated;
466
+ updated.splice(index, 1);
467
+ nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
468
+ return ensureFirstCardDefaultPhoneSuggestions(updated);
469
+ });
470
+ });
471
+ setCarouselErrors((prev = []) => {
472
+ const next = Array.isArray(prev) ? [...prev] : [];
473
+ next.splice(index, 1);
474
+ return next;
475
+ });
476
+ setActiveCarouselIndex(`${nextIdx}`);
477
+ };
478
+
479
+
480
+ const addCarouselCard = () => {
481
+ let newIndex = 0;
482
+ flushSync(() => {
483
+ setCarouselData((prev = []) => {
484
+ const updated = cloneDeep(prev);
485
+ updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
486
+ newIndex = updated.length - 1;
487
+ return updated;
488
+ });
489
+ });
490
+ setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
491
+ setActiveCarouselIndex(`${newIndex}`);
492
+ };
493
+
494
+ // Initialize carousel data when switching to carousel type
495
+ useEffect(() => {
496
+ if (!isCarouselType) return;
497
+ if (!carouselData || carouselData.length === 0) {
498
+ setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
499
+ setCarouselErrors([{}]);
500
+ setActiveCarouselIndex('0');
501
+ }
502
+ }, [isCarouselType]);
503
+
504
+ // keep derived carousel key in sync
505
+ useEffect(() => {
506
+ if (!isCarouselType) return;
507
+ if (!selectedCarouselHeight || !selectedCarouselWidth) return;
508
+ // Required format: HEIGHT_WIDTH
509
+ setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
510
+ }, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
511
+
512
+ const renderCarouselDimensionSelection = () => {
513
+ if (!isCarouselType) return null;
514
+ return (
515
+ <CarouselDimensionSelection
516
+ isEditFlow={isEditFlow}
517
+ isFullMode={isFullMode}
518
+ selectedCarouselHeight={selectedCarouselHeight}
519
+ selectedCarouselWidth={selectedCarouselWidth}
520
+ selectedCarousel={selectedCarousel}
521
+ onHeightChange={(val) => {
522
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
523
+ resetCarouselMediaForAllCards();
524
+ setSelectedCarouselHeight(val);
525
+ }}
526
+ onWidthChange={(val) => {
527
+ // Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
528
+ resetCarouselMediaForAllCards();
529
+ setSelectedCarouselWidth(val);
530
+ }}
531
+ />
532
+ );
533
+ };
534
+
535
+ const getCarouselTabPanes = () => {
536
+ const tagsForPicker = getRcsTags();
537
+ return (carouselData || []).map((card, index) => ({
538
+ key: String(index),
539
+ tab: index + 1,
540
+ content: (
541
+ <CarouselCard
542
+ cardIndex={index}
543
+ card={card}
544
+ carouselLength={carouselData.length}
545
+ isEditFlow={isEditFlow}
546
+ isFullMode={isFullMode}
547
+ mediaRadioOptions={mediaRadioOptions}
548
+ cardError={carouselErrors?.[index]}
549
+ location={location}
550
+ tagsForPicker={tagsForPicker}
551
+ injectedTags={injectedTags}
552
+ selectedOfferDetails={selectedOfferDetails}
553
+ formatMessage={formatMessage}
554
+ titleMaxLength={getTitleMaxLength()}
555
+ descriptionMaxLength={getDescriptionMaxLength()}
556
+ titleCharCount={getCarouselTitleCharacterCount(carouselData, index)}
557
+ descriptionCharCount={getCarouselDescriptionCharacterCount(carouselData, index)}
558
+ dimKey={getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth)}
559
+ rcsData={rcsData}
560
+ uploadRcsImage={uploadRcsImage}
561
+ uploadRcsVideo={uploadRcsVideo}
562
+ hostName={hostName}
563
+ onDelete={() => deleteCarouselCard(index)}
564
+ onMediaTypeChange={(value) => {
565
+ // Reset media fields when switching type
566
+ if (value === RCS_MEDIA_TYPES.IMAGE) {
567
+ // Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
568
+ clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
569
+ handleCarouselValueChange(index, [
570
+ { fieldName: 'mediaType', value },
571
+ { fieldName: 'videoAsset', value: {} },
572
+ { fieldName: 'thumbnailSrc', value: '' },
573
+ ]);
574
+ } else {
575
+ // Switching to VIDEO: clear image upload so it doesn't auto-restore.
576
+ clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
577
+ handleCarouselValueChange(index, [
578
+ { fieldName: 'mediaType', value },
579
+ { fieldName: 'imageSrc', value: '' },
580
+ ]);
581
+ }
582
+ }}
583
+ onTitleChange={({ target: { value } }) => {
584
+ let error = false;
585
+ if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
586
+ error = formatMessage(messages.templateHeaderLengthError);
587
+ } else {
588
+ error = variableErrorHandling(value);
589
+ }
590
+ updateCarouselErrors(index, { title: error });
591
+ handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
592
+ }}
593
+ onDescriptionChange={({ target: { value } }) => {
594
+ let error = false;
595
+ if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
596
+ error = formatMessage(messages.templateMessageLengthError);
597
+ } else {
598
+ error = variableErrorHandling(value);
599
+ }
600
+ updateCarouselErrors(index, { description: error });
601
+ handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
602
+ }}
603
+ onTitleAddVar={() => appendVarToCarouselField(index, 'title')}
604
+ onDescriptionAddVar={() => appendVarToCarouselField(index, 'description')}
605
+ onTitleTagSelect={onCarouselTitleTagSelect}
606
+ onDescTagSelect={onCarouselDescTagSelect}
607
+ onContextChange={handleOnTagsContextChange}
608
+ renderEditMessage={renderCarouselEditMessage}
609
+ onFieldChange={(fieldName, value) => handleCarouselValueChange(index, [{ fieldName, value }])}
610
+ onSuggestionsChange={(nextSuggestions) =>
611
+ handleCarouselValueChange(index, [{ fieldName: 'suggestions', value: nextSuggestions }])}
612
+ />
613
+ ),
614
+ }));
615
+ };
616
+
617
+ const renderCarouselSection = () => {
618
+ if (!isCarouselType) return null;
619
+
620
+ const isMaxCarouselCardsReached = MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0);
621
+ const addCardTooltipTitle = isMaxCarouselCardsReached
622
+ ? formatMessage(messages.rcsCarouselMaxCardTooltip, { max: MAX_RCS_CAROUSEL_ALLOWED })
623
+ // "Set up the first card" only makes sense while creating a new template —
624
+ // in edit flow the cards already exist, so don't show it there.
625
+ : (!isEditFlow && checkDisableAddCarouselButton(carouselData, activeCarouselIndex) ? formatMessage(messages.rcsCarouselAddCardDisabledTooltip) : '');
626
+
627
+ // No "+" in edit flow (cards can't be added there) — and with no "+", the
628
+ // divider before it has nothing to separate, so skip both together instead
629
+ // of rendering a dangling divider.
630
+ const operations = isEditFlow ? null : (
631
+ <>
632
+ <CapDivider type="vertical" />
633
+ <CapTooltip title={addCardTooltipTitle}>
634
+ <span className="button-disabled-tooltip-wrapper">
635
+ <CapButton
636
+ onClick={addCarouselCard}
637
+ type="flat"
638
+ className="add-carousel-content-button"
639
+ disabled={
640
+ isEditFlow ||
641
+ !isFullMode ||
642
+ isMaxCarouselCardsReached ||
643
+ checkDisableAddCarouselButton(carouselData, activeCarouselIndex)
644
+ }
645
+ >
646
+ <CapIcon type="plus" />
647
+ </CapButton>
648
+ </span>
649
+ </CapTooltip>
650
+ </>
651
+ );
652
+
653
+ return (
654
+ <CapRow className={`rcs-carousel-section ${isEditFlow ? 'rcs-carousel-section--readonly-above' : ''}`}>
655
+ {renderCarouselDimensionSelection()}
656
+ <CapRow className="rcs-carousel-tab">
657
+ <CapTab
658
+ key={`rcs-carousel-tab-${carouselResetNonce}`}
659
+ defaultActiveKey="0"
660
+ activeKey={activeCarouselIndex}
661
+ tabBarExtraContent={operations}
662
+ onChange={(key) => {
663
+ setActiveCarouselIndex(`${key}`);
664
+ // reset focused var when switching cards (as per requirement)
665
+ setCarouselTitleFocusedVarId('');
666
+ setCarouselDescFocusedVarId('');
667
+ }}
668
+ panes={getCarouselTabPanes()}
669
+ />
670
+ </CapRow>
671
+ </CapRow>
672
+ );
673
+ };
325
674
 
326
675
  const mediaRadioOptions = [
327
676
  {
@@ -330,11 +679,16 @@ export const Rcs = (props) => {
330
679
  },
331
680
  {
332
681
  value: RCS_MEDIA_TYPES.VIDEO,
333
- label: formatMessage(messages.mediaVideo),
682
+ label: formatMessage(
683
+ templateType === contentType.carousel
684
+ ? messages.carouselMediaVideoOption
685
+ : messages.mediaVideo,
686
+ ),
334
687
  },
335
688
  ];
336
689
  const aiContentBotDisabled = isAiContentBotDisabled();
337
690
 
691
+
338
692
  const updateButtonChange = (data, index) => {
339
693
  if (data && data.text) {
340
694
  const forbiddenError = forbiddenCharactersValidation(data.text);
@@ -371,7 +725,9 @@ export const Rcs = (props) => {
371
725
  if (isFullMode) return;
372
726
  if (loadingTags || !tags || tags.length === 0) return;
373
727
  const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
374
- const resolved = resolveTemplateWithMap(templateStr); // placeholders -> mapped value (or '')
728
+ const slotOffset =
729
+ type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
730
+ const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
375
731
  if (!resolved) {
376
732
  if (type === TITLE_TEXT) setTemplateTitleError(false);
377
733
  if (type === MESSAGE_TEXT) setTemplateDescError(false);
@@ -380,8 +736,8 @@ export const Rcs = (props) => {
380
736
 
381
737
  let contentForValidation = resolved;
382
738
  const placeholderTokens = templateStr.match(rcsVarRegex) || [];
383
- placeholderTokens.forEach((t) => {
384
- const escaped = t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
739
+ placeholderTokens.forEach((token) => {
740
+ const escaped = token.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
385
741
  contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
386
742
  });
387
743
  if (!contentForValidation.trim()) {
@@ -398,10 +754,16 @@ export const Rcs = (props) => {
398
754
  tagModule: getDefaultTags,
399
755
  isFullMode,
400
756
  }) || {};
401
- const errorMsg =
402
- (validationResponse?.isBraceError &&
403
- formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
404
- false;
757
+ const unsupportedTagsLengthCheck =
758
+ validationResponse?.unsupportedTags?.length > 0;
759
+ const errorMsg =
760
+ (unsupportedTagsLengthCheck &&
761
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
762
+ unsupportedTags: validationResponse.unsupportedTags,
763
+ })) ||
764
+ (validationResponse.isBraceError &&
765
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
766
+ false;
405
767
  if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
406
768
  if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
407
769
  };
@@ -414,55 +776,238 @@ export const Rcs = (props) => {
414
776
  validateResolvedTagsForType(MESSAGE_TEXT);
415
777
  }, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
416
778
 
779
+ useEffect(() => {
780
+ if (isFullMode || !isCarouselType) return;
781
+ if (loadingTags || !tags || tags.length === 0) return;
782
+ (carouselData || []).forEach((card, idx) => {
783
+ ['title', 'description'].forEach((field) => {
784
+ const templateStr = card?.[field] || '';
785
+ if (!templateStr) return;
786
+ const resolved = resolveCarouselTemplateWithMap(templateStr, idx, field === 'description');
787
+ if (!resolved) {
788
+ updateCarouselErrors(idx, { [field]: false });
789
+ return;
790
+ }
791
+ let contentForValidation = resolved;
792
+ const placeholderTokens = templateStr.match(rcsVarRegex) || [];
793
+ placeholderTokens.forEach((token) => {
794
+ const escaped = token.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
795
+ contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
796
+ });
797
+ if (!contentForValidation.trim()) {
798
+ updateCarouselErrors(idx, { [field]: false });
799
+ return;
800
+ }
801
+ const validationResponse = validateTags({
802
+ content: contentForValidation,
803
+ tagsParam: tags,
804
+ injectedTagsParams: injectedTags,
805
+ location,
806
+ tagModule: getDefaultTags,
807
+ eventContextTags,
808
+ isFullMode,
809
+ }) || {};
810
+ const errorMsg =
811
+ (validationResponse?.unsupportedTags?.length > 0 &&
812
+ formatMessage(globalMessages.unsupportedTagsValidationError, {
813
+ unsupportedTags: validationResponse.unsupportedTags,
814
+ })) ||
815
+ (validationResponse.isBraceError &&
816
+ formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
817
+ false;
818
+ updateCarouselErrors(idx, { [field]: errorMsg });
819
+ });
820
+ });
821
+ }, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
822
+
417
823
  const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
418
824
 
419
- const resolveTemplateWithMap = (str = '') => {
825
+ const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
826
+
827
+ /** Same `{{tag}}` in both title and description must not share one semantic map entry. */
828
+ const rcsSpanningSemanticVarNames = useMemo(
829
+ () => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
830
+ [templateTitle, templateDesc],
831
+ );
832
+
833
+ /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
834
+ const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
835
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
836
+ const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
837
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
838
+ let varOrdinal = 0;
839
+ for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
840
+ const segmentToken = templateSegments[segmentIndexInField];
841
+ if (rcsVarTestRegex.test(segmentToken)) {
842
+ if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
843
+ return offset + varOrdinal;
844
+ }
845
+ varOrdinal += 1;
846
+ }
847
+ }
848
+ return null;
849
+ };
850
+
851
+ /**
852
+ * Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
853
+ * multi-field variable resolution.
854
+ */
855
+ const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
420
856
  if (!str) return '';
421
- const arr = splitTemplateVarString(str);
857
+ const arr = splitTemplateVarStringRcs(str);
858
+ let varOrdinal = 0;
422
859
  return arr.map((elem) => {
423
860
  if (rcsVarTestRegex.test(elem)) {
424
861
  const key = getVarNameFromToken(elem);
425
- const v = cardVarMapped?.[key];
426
- if (isNil(v) || String(v)?.trim?.() === '') return elem;
427
- return String(v);
862
+ const globalSlot = slotOffset + varOrdinal;
863
+ varOrdinal += 1;
864
+ const slotValue = resolveCardVarMappedSlotValue(
865
+ cardVarMapped,
866
+ key,
867
+ globalSlot,
868
+ isEditLike,
869
+ rcsSpanningSemanticVarNames.has(key),
870
+ );
871
+ if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
872
+ return String(slotValue);
428
873
  }
429
874
  return elem;
430
875
  }).join('');
431
876
  };
432
877
 
878
+ /**
879
+ * Carousel resolve: each variable slot's key comes from `computeCarouselVarSlotKeys` (title,
880
+ * then description, per card; same name within a card shares one key; different cards never
881
+ * collide). A slot's key never depends on which tag currently fills it.
882
+ */
883
+ const resolveCarouselTemplateWithMap = (str = '', cardIndex, isDescField = false) => {
884
+ if (!str) return '';
885
+ const arr = splitTemplateVarStringRcs(str);
886
+ const fieldKeys = (isDescField
887
+ ? computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex]?.description
888
+ : computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex]?.title) ?? [];
889
+ let varOrdinal = 0;
890
+ return arr.map((elem) => {
891
+ if (rcsVarTestRegex.test(elem)) {
892
+ const slotKey = fieldKeys[varOrdinal];
893
+ varOrdinal += 1;
894
+ const slotValue = cardVarMapped?.[slotKey];
895
+ if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
896
+ return String(slotValue);
897
+ }
898
+ return elem;
899
+ }).join('');
900
+ };
433
901
 
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]);
902
+ const buildCarouselCardsForPreview = (cards = []) =>
903
+ buildCarouselCardsForPreviewUtil(cards, { isFullMode, rcsVarRegex, resolveTemplateWithMap });
454
904
 
905
+ /**
906
+ * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
907
+ * (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
908
+ * TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
909
+ */
910
+ const getTemplateContent = useCallback(() => {
911
+ if (templateType === contentType.carousel) {
912
+ const carouselDimKey = getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth);
913
+ const carouselImgDims =
914
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
915
+ const carouselVidDims =
916
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
917
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
918
+ return {
919
+ carouselData: buildCarouselCardsForPreview(carouselData),
920
+ carouselPreviewDimensions: {
921
+ imageWidth: carouselImgDims.width,
922
+ imageHeight: carouselImgDims.height,
923
+ videoThumbWidth: carouselVidDims.width,
924
+ videoThumbHeight: carouselVidDims.height,
925
+ },
926
+ };
927
+ }
928
+ const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
929
+ const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
930
+ const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
931
+
932
+ const isSlotMappingMode = isEditFlow || !isFullMode;
933
+ const titleVarCountForResolve = isMediaTypeText
934
+ ? 0
935
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
936
+ const resolvedTitle = isMediaTypeText
937
+ ? ''
938
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
939
+ const resolvedDesc = isSlotMappingMode
940
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
941
+ : templateDesc;
942
+
943
+ const mediaPreview = {};
944
+ if (isMediaTypeImage && rcsImageSrc) {
945
+ mediaPreview.rcsImageSrc = rcsImageSrc;
946
+ }
947
+ if (isMediaTypeVideo && !isMediaTypeText) {
948
+ if (rcsThumbnailSrc) {
949
+ mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
950
+ } else if (rcsVideoSrc?.videoSrc) {
951
+ mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
952
+ }
953
+ if (rcsThumbnailSrc) {
954
+ mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
955
+ }
956
+ }
957
+
958
+ const contentObj = {
959
+ templateHeader: resolvedTitle,
960
+ templateMessage: resolvedDesc,
961
+ ...mediaPreview,
962
+ ...(suggestions.length > 0 && {
963
+ suggestions: suggestions,
964
+ }),
965
+ };
966
+
967
+ return contentObj;
968
+ }, [
969
+ templateMediaType,
970
+ templateTitle,
971
+ templateDesc,
972
+ rcsImageSrc,
973
+ rcsVideoSrc,
974
+ rcsThumbnailSrc,
975
+ suggestions,
976
+ selectedDimension,
977
+ isFullMode,
978
+ isEditFlow,
979
+ cardVarMapped,
980
+ rcsSpanningSemanticVarNames,
981
+ carouselData,
982
+ selectedCarouselHeight,
983
+ selectedCarouselWidth,
984
+ ]);
985
+
986
+ const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
455
987
 
456
- const RcsLabel = styled.div`
457
- display: flex;
458
- margin-top: 20px;
459
- `;
460
988
  const paramObj = params || {};
461
989
  useEffect(() => {
462
990
  const { id } = paramObj;
463
991
  if (id && isFullMode) {
464
992
  setSpin(true);
465
993
  actions.getTemplateDetails(id, setSpin);
994
+ } else if (!id && isFullMode) {
995
+ // Create New: clear standalone media and ALL possible carousel card Redux slots.
996
+ // Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
997
+ updateRcsImageSrc('');
998
+ setRcsVideoSrc({});
999
+ setRcsThumbnailSrc('');
1000
+ setAssetList({});
1001
+ setEditFlow(false);
1002
+ actions.clearRcsMediaAsset(0);
1003
+ actions.clearRcsMediaAsset(1);
1004
+ for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
1005
+ actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
1006
+ actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
1007
+ actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
1008
+ }
1009
+ setCarouselData([]);
1010
+ setCarouselErrors([]);
466
1011
  }
467
1012
  return () => {
468
1013
  actions.clearEditResponse();
@@ -470,59 +1015,68 @@ export const Rcs = (props) => {
470
1015
  }, [paramObj.id]);
471
1016
 
472
1017
  useEffect(() => {
473
- if (!(isEditFlow || !isFullMode)) return;
1018
+ if (!(isEditFlow || !isFullMode)) return;
474
1019
 
475
- const initField = (targetString, currentVarMap, setVarMap, setUpdated) => {
476
- const arr = splitTemplateVarString(targetString);
477
- if (!arr?.length) {
478
- setVarMap({});
479
- setUpdated([]);
480
- return;
1020
+ const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
1021
+
1022
+ const initField = (targetString, setVarMap, slotOffset) => {
1023
+ const arr = splitTemplateVarStringRcs(targetString);
1024
+ if (!arr?.length) {
1025
+ setVarMap({});
1026
+ return;
1027
+ }
1028
+ const nextVarMap = {};
1029
+ let varOrdinal = 0;
1030
+ arr.forEach((elem, idx) => {
1031
+ // Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
1032
+ if (rcsVarTestRegex.test(elem)) {
1033
+ const id = `${elem}_${idx}`;
1034
+ const varName = getVarNameFromToken(elem);
1035
+ const globalSlot = slotOffset + varOrdinal;
1036
+ varOrdinal += 1;
1037
+ const mappedValue = resolveCardVarMappedSlotValue(
1038
+ cardVarMapped,
1039
+ varName,
1040
+ globalSlot,
1041
+ isEditLike,
1042
+ rcsSpanningSemanticVarNames.has(varName),
1043
+ );
1044
+ nextVarMap[id] = mappedValue;
481
1045
  }
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
- };
1046
+ });
1047
+ setVarMap(nextVarMap);
1048
+ };
501
1049
 
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){
1050
+ initField(templateTitle, setTitleVarMappedData, 0);
1051
+ initField(templateDesc, setDescVarMappedData, titleTokenCount);
1052
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
1053
+
1054
+ useEffect(() => {
1055
+ if (!isEditFlow && isFullMode) {
508
1056
  setRcsVideoSrc({});
509
1057
  updateRcsImageSrc('');
510
- setUpdateRcsImageSrc('');
511
- updateRcsThumbnailSrc('');
1058
+ setRcsThumbnailSrc('');
512
1059
  setAssetList({});
513
- }
514
- }, [templateMediaType]);
1060
+ }
1061
+ }, [templateMediaType]);
515
1062
 
516
- const templateStatusHelper = (details) => {
517
- const status = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
518
- switch (status) {
1063
+ /** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
1064
+ const templateStatusHelper = (cardContentFirst) => {
1065
+ const raw =
1066
+ cardContentFirst?.Status
1067
+ ?? cardContentFirst?.status
1068
+ ?? cardContentFirst?.approvalStatus
1069
+ ?? '';
1070
+ const status = typeof raw === 'string' ? raw.trim() : String(raw);
1071
+ const normalizedStatus = status.toLowerCase();
1072
+ switch (normalizedStatus) {
519
1073
  case RCS_STATUSES.approved:
520
1074
  setTemplateStatus(RCS_STATUSES.approved);
521
1075
  break;
522
1076
  case RCS_STATUSES.pending:
523
1077
  setTemplateStatus(RCS_STATUSES.pending);
524
1078
  break;
525
- case RCS_STATUSES.awaitingApproval:
1079
+ case RCS_STATUSES.awaitingApproval.toLowerCase():
526
1080
  setTemplateStatus(RCS_STATUSES.awaitingApproval);
527
1081
  break;
528
1082
  case RCS_STATUSES.unavailable:
@@ -531,6 +1085,9 @@ export const Rcs = (props) => {
531
1085
  case RCS_STATUSES.rejected:
532
1086
  setTemplateStatus(RCS_STATUSES.rejected);
533
1087
  break;
1088
+ case RCS_STATUSES.created:
1089
+ setTemplateStatus(RCS_STATUSES.created);
1090
+ break;
534
1091
  default:
535
1092
  setTemplateStatus(status);
536
1093
  break;
@@ -584,48 +1141,261 @@ export const Rcs = (props) => {
584
1141
  };
585
1142
 
586
1143
  useEffect(() => {
587
- const details = isFullMode ? rcsData?.templateDetails : templateData;
1144
+ const details = rcsHydrationDetails;
588
1145
  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);
1146
+ // Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
1147
+ const cardFromVersions = get(
1148
+ details,
1149
+ 'versions.base.content.RCS.rcsContent.cardContent[0]',
1150
+ );
1151
+ const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
1152
+ const cardType = (rcsContent?.cardType || '').toString();
1153
+
1154
+ setEditFlow(true);
1155
+ setTemplateName(details?.name || details?.creativeName || '');
1156
+
1157
+ const cardFromTop = get(details, 'rcsContent.cardContent[0]');
1158
+ const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
1159
+ const cardVarMappedFromCardContent =
1160
+ card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
1161
+ ? card0.cardVarMapped
1162
+ : {};
1163
+ const cardVarMappedFromRootMirror =
1164
+ details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
1165
+ ? details.rcsCardVarMapped
1166
+ : {};
1167
+ // Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
1168
+ // nested versions.cardContent[0].cardVarMapped is dropped on reload.
1169
+ const mergedCardVarMappedFromPayload = {
1170
+ ...cardVarMappedFromRootMirror,
1171
+ ...cardVarMappedFromCardContent,
1172
+ };
1173
+ const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
1174
+ const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
1175
+ const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
1176
+ Object.keys(mergedCardVarMappedFromPayload)
1177
+ .sort()
1178
+ .reduce((accumulator, mapKey) => {
1179
+ accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
1180
+ return accumulator;
1181
+ }, {}),
1182
+ )}`;
1183
+ if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
1184
+ lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
1185
+ setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
1186
+ }
1187
+ const tokenListForMap = [
1188
+ ...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
1189
+ ...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
1190
+ ];
1191
+ const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
1192
+ // Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
1193
+ // getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
1194
+ const cardVarMappedBeforeCoalesce = isFullMode
1195
+ ? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
1196
+ : { ...mergedCardVarMappedFromPayload };
1197
+ const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
1198
+ cardVarMappedBeforeCoalesce,
1199
+ loadedTitleForMap,
1200
+ loadedDescForMap,
1201
+ rcsVarRegex,
1202
+ );
1203
+ const cardVarMappedAfterNumericSlotSync = !isFullMode
1204
+ ? syncCardVarMappedSemanticsFromSlots(
1205
+ cardVarMappedAfterCoalesce,
1206
+ loadedTitleForMap,
1207
+ loadedDescForMap,
1208
+ rcsVarRegex,
1209
+ )
1210
+ : cardVarMappedAfterCoalesce;
1211
+ const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
1212
+ // Pre-populate variable/tag mappings while opening an existing template in edit flows
1213
+ setCardVarMapped((previousVarMapState) => {
1214
+ const previousVarMap = previousVarMapState ?? {};
1215
+ if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
1216
+ const previousVarMapKeys = Object.keys(previousVarMap);
1217
+ const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
1218
+ if (previousVarMapKeys.length === nextVarMapKeys.length) {
1219
+ const allSlotValuesMatchPrevious = previousVarMapKeys.every(
1220
+ (key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
1221
+ );
1222
+ if (allSlotValuesMatchPrevious) return previousVarMapState;
1223
+ }
1224
+ return hydratedCardVarMappedResult;
1225
+ });
1226
+
1227
+ if (cardType.toUpperCase() === contentType.carousel) {
1228
+
1229
+ setTemplateType(contentType.carousel);
1230
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1231
+ // Library/campaign payloads can store cardSettings/cardContent at the flat top-level
1232
+ // `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
1233
+ // used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
1234
+ // silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
1235
+ // actually uploaded at and causing object-fit:cover to crop the preview image.
1236
+ const cardSettings = rcsContent?.cardSettings
1237
+ || get(details, 'rcsContent.cardSettings', {});
1238
+ const cardWidth = cardSettings?.cardWidth || SMALL;
1239
+ setSelectedCarouselWidth(cardWidth);
1240
+
1241
+ const cards = Array.isArray(rcsContent?.cardContent)
1242
+ ? rcsContent.cardContent
1243
+ : get(details, 'rcsContent.cardContent', []);
1244
+ const firstHeight = cards?.[0]?.media?.height || MEDIUM;
1245
+ setSelectedCarouselHeight(firstHeight);
1246
+ setSelectedCarousel(`${firstHeight}_${cardWidth}`);
1247
+ setActiveCarouselIndex('0');
1248
+
1249
+ if (cards.length > 1) {
1250
+ // Each card's own `cardVarMapped` is already keyed by global var position (see
1251
+ // computeCarouselVarSlotKeys) — a slot's key is assigned once, from where its token
1252
+ // sits in the template text, and never depends on which tag fills it. So merging every
1253
+ // card's own map directly is correct and collision-free: no per-card coalescing,
1254
+ // normalizing, or re-scoping needed (unlike the old name-based scoped-key scheme).
1255
+ setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}) => {
1256
+ const rawCardMap = c?.cardVarMapped;
1257
+ if (rawCardMap == null || typeof rawCardMap !== 'object') return merged;
1258
+ return { ...merged, ...rawCardMap };
1259
+ }, { ...(previousVarMap || {}) }));
1260
+ }
1261
+
1262
+ const hydratedCards = cards.map((c = {}, idx) => {
1263
+ const mediaType = c.mediaType;
1264
+ const media = c.media || {};
1265
+ const mediaUrl = media.mediaUrl || '';
1266
+ const thumbUrl = media.thumbnailUrl || '';
1267
+ const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
1268
+ const suggestions = idx === 0 && rawSuggestions.length === 0
1269
+ ? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
1270
+ : rawSuggestions;
1271
+ return {
1272
+ title: c.title || '',
1273
+ description: c.description || '',
1274
+ mediaType,
1275
+ imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
1276
+ videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
1277
+ videoSrc: mediaUrl,
1278
+ previewUrl: thumbUrl,
1279
+ videoThumbnail: thumbUrl,
1280
+ videoName: c?.media?.videoName || '',
1281
+ } : {},
1282
+ thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
1283
+ suggestions,
1284
+ };
1285
+ });
1286
+ setCarouselData(
1287
+ hydratedCards.length > 0
1288
+ ? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
1289
+ : [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
1290
+ );
1291
+ setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
1292
+
1293
+ // Status bar uses first card's Status, keep existing behavior.
1294
+ if (isHostInfoBip) {
1295
+ setTemplateStatus('');
1296
+ } else {
1297
+ const firstCard = cards?.[0] || {};
1298
+ const cardForCarouselStatus = {
1299
+ ...firstCard,
1300
+ Status:
1301
+ firstCard.Status
1302
+ ?? firstCard.status
1303
+ ?? firstCard.approvalStatus
1304
+ ?? get(details, 'templateStatus')
1305
+ ?? get(details, 'approvalStatus')
1306
+ ?? get(details, 'creativeStatus')
1307
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1308
+ ?? '',
1309
+ };
1310
+ templateStatusHelper(cardForCarouselStatus);
1311
+ }
1312
+
1313
+ // Hydrate SMS fallback for carousel — same logic as for non-carousel types below.
1314
+ // Without this the early `return` skips fallback hydration entirely, so reopening
1315
+ // a carousel template never restores the saved SMS fallback data.
1316
+ applySmsFallbackHydration({
1317
+ details,
1318
+ isFullMode,
1319
+ lastSmsFallbackHydrationKeyRef,
1320
+ setSmsFallbackData,
1321
+ });
1322
+
1323
+ return;
592
1324
  }
593
- const mediaType = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
594
- if (mediaType === RCS_MEDIA_TYPES.NONE) {
1325
+
1326
+ const mediaType =
1327
+ card0.mediaType
1328
+ || get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
1329
+ if (cardType.toUpperCase() !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
595
1330
  setTemplateType(contentType.text_message);
596
- } else {
1331
+ } else if (cardType.toUpperCase() !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
597
1332
  setTemplateType(contentType.rich_card);
598
1333
  }
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;
1334
+
1335
+ const loadedTitle = loadedTitleForMap;
1336
+ const loadedDesc = loadedDescForMap;
1337
+ const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
1338
+ loadedTitle,
1339
+ loadedDesc,
1340
+ isFullMode,
1341
+ cardVarMappedAfterHydration: hydratedCardVarMappedResult,
1342
+ rcsVarRegex,
1343
+ });
606
1344
  setTemplateTitle(normalizedTitle);
607
1345
  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', '');
1346
+ setSuggestions(
1347
+ Array.isArray(card0.suggestions)
1348
+ ? card0.suggestions
1349
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
1350
+ );
1351
+ const cardForStatus = {
1352
+ ...card0,
1353
+ Status:
1354
+ card0.Status
1355
+ ?? card0.status
1356
+ ?? card0.approvalStatus
1357
+ ?? get(details, 'templateStatus')
1358
+ ?? get(details, 'approvalStatus')
1359
+ ?? get(details, 'creativeStatus')
1360
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
1361
+ ?? '',
1362
+ };
1363
+ if (isHostInfoBip) {
1364
+ setTemplateStatus('');
1365
+ } else {
1366
+ templateStatusHelper(cardForStatus);
1367
+ }
1368
+ const mediaData =
1369
+ card0.media != null && card0.media !== ''
1370
+ ? card0.media
1371
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
1372
+ const cardSettings =
1373
+ get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
1374
+ || get(details, 'rcsContent.cardSettings', '');
612
1375
  setMediaData(mediaData, mediaType, cardSettings);
613
1376
  if (details?.edit) {
614
1377
  const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
615
1378
  setRcsAccount(rcsAccountId);
616
1379
  }
1380
+
1381
+ applySmsFallbackHydration({
1382
+ details,
1383
+ isFullMode,
1384
+ lastSmsFallbackHydrationKeyRef,
1385
+ setSmsFallbackData,
1386
+ });
617
1387
  }
618
- }, [rcsData, templateData, isFullMode, isEditFlow]);
619
-
1388
+ }, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
620
1389
 
621
1390
  useEffect(() => {
622
1391
  if (templateType === contentType.text_message) {
623
1392
  setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
624
- setTemplateTitle('');
625
- setTemplateTitleError('');
1393
+ // Full-mode create only: switching to plain text clears draft title/media. Never clear when
1394
+ // hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
626
1395
  if (!isEditFlow && isFullMode) {
1396
+ setTemplateTitle('');
1397
+ setTemplateTitleError('');
627
1398
  setUpdateRcsImageSrc('');
628
- setUpdateRcsVideoSrc({});
629
1399
  setRcsVideoSrc({});
630
1400
  setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
631
1401
  }
@@ -649,23 +1419,22 @@ export const Rcs = (props) => {
649
1419
  }, [templateType, isEditFlow, isFullMode]);
650
1420
 
651
1421
  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);
1422
+ const { type, module } = location.query || {};
1423
+ const isEmbedded = type === EMBEDDED;
1424
+ // Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
1425
+ const context = isEmbedded ? module : 'outbound';
1426
+ const embedded = isEmbedded ? type : FULL;
1427
+ const query = {
1428
+ layout: SMS,
1429
+ type: TAG,
1430
+ context,
1431
+ embedded,
1432
+ };
1433
+ if (getDefaultTags) {
1434
+ query.context = getDefaultTags;
667
1435
  }
668
- }, [showDltContainer]);
1436
+ fetchTagSchemaIfNewQuery(query);
1437
+ }, [fetchTagSchemaIfNewQuery]);
669
1438
 
670
1439
  useEffect(() => {
671
1440
  let tag = get(metaEntities, `tags.standard`, []);
@@ -688,39 +1457,167 @@ export const Rcs = (props) => {
688
1457
  context,
689
1458
  embedded,
690
1459
  };
1460
+ if (getDefaultTags) {
1461
+ query.context = getDefaultTags;
1462
+ }
1463
+ fetchTagSchemaIfNewQuery(query);
691
1464
  globalActions.fetchSchemaForEntity(query);
692
1465
  };
693
1466
 
694
- const onTagSelect = (data, areaId) => {
695
- if (!areaId) return;
696
- const sep = areaId.lastIndexOf('_');
697
- if (sep === -1) return;
698
- const numId = Number(areaId.slice(sep + 1));
699
- if (isNaN(numId)) return;
700
- const token = areaId.slice(0, sep);
701
- const variableName = getVarNameFromToken(token);
702
- if (!variableName) return;
703
- setCardVarMapped((prev) => {
704
- const base = (prev?.[variableName] ?? '').toString();
705
- const nextVal = `${base}{{${data}}}`;
706
- return {
707
- ...(prev || {}),
708
- [variableName]: nextVal,
709
- };
1467
+ const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
1468
+ if (!templateStr || !numericVarName || !tagName) return templateStr;
1469
+ const placeholderRegex = buildRcsNumericMustachePlaceholderRegex(numericVarName);
1470
+ return templateStr.replace(placeholderRegex, `{{${tagName}}}`);
1471
+ };
1472
+
1473
+ const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
1474
+ if (!varSegmentCompositeDomId) return;
1475
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
1476
+ if (underscoreIndexInCompositeId === -1) return;
1477
+ const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
1478
+ if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
1479
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
1480
+ const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
1481
+ if (!semanticOrNumericVarName) return;
1482
+ const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
1483
+ const templateStringForField =
1484
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
1485
+ const titleOrMessageFieldType =
1486
+ tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
1487
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
1488
+ varSegmentCompositeDomId,
1489
+ templateStringForField,
1490
+ titleOrMessageFieldType,
1491
+ );
1492
+ const cardVarMappedNumericSlotKey =
1493
+ globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
1494
+ ? String(globalVarSlotIndexZeroBased + 1)
1495
+ : null;
1496
+
1497
+ setCardVarMapped((previousCardVarMapped) => {
1498
+ const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
1499
+ if (isNumericPlaceholderSlot) {
1500
+ const existingValueBeforeAppend = (
1501
+ previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
1502
+ ).toString();
1503
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1504
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1505
+ updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
1506
+ } else {
1507
+ // Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
1508
+ // "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
1509
+ // read/write the global numeric slot only and drop the shared semantic key.
1510
+ const existingValueBeforeAppend = cardVarMappedNumericSlotKey
1511
+ ? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
1512
+ : String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
1513
+ const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
1514
+ delete updatedCardVarMapped[semanticOrNumericVarName];
1515
+ if (cardVarMappedNumericSlotKey) {
1516
+ updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
1517
+ // Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
1518
+ const fieldOffset = titleOrMessageFieldType === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
1519
+ const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
1520
+ let varOrdinal = 0;
1521
+ for (let i = 0; i < fieldSegments.length; i++) {
1522
+ if (rcsVarTestRegex.test(fieldSegments[i])) {
1523
+ const slotVarName = getVarNameFromToken(fieldSegments[i]);
1524
+ const globalSlot = fieldOffset + varOrdinal;
1525
+ varOrdinal += 1;
1526
+ if (slotVarName === semanticOrNumericVarName && globalSlot !== globalVarSlotIndexZeroBased) {
1527
+ updatedCardVarMapped[String(globalSlot + 1)] = mappedValueAfterAppendingTag;
1528
+ }
1529
+ }
1530
+ }
1531
+ } else {
1532
+ updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
1533
+ }
1534
+ }
1535
+ return updatedCardVarMapped;
710
1536
  });
1537
+
1538
+ if (
1539
+ isNumericPlaceholderSlot
1540
+ && (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
1541
+ ) {
1542
+ if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
1543
+ setTemplateTitle((previousTitle) => {
1544
+ const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
1545
+ previousTitle || '',
1546
+ semanticOrNumericVarName,
1547
+ selectedTagNameFromPicker,
1548
+ );
1549
+ if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
1550
+ setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
1551
+ // 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
1552
+ setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
1553
+ return titleAfterReplacingNumericPlaceholder;
1554
+ });
1555
+ } else {
1556
+ setTemplateDesc((previousDescription) => {
1557
+ const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
1558
+ previousDescription || '',
1559
+ semanticOrNumericVarName,
1560
+ selectedTagNameFromPicker,
1561
+ );
1562
+ if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
1563
+ return previousDescription;
1564
+ }
1565
+ setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
1566
+ setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
1567
+ return descriptionAfterReplacingNumericPlaceholder;
1568
+ });
1569
+ }
1570
+ }
711
1571
  };
712
1572
 
713
- const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
1573
+ const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
1574
+
1575
+ const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
714
1576
 
715
- const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
1577
+ /** Field-local var ordinal (0-based, vars only) for a `{{tok}}_segIdx` composite id — scoped to
1578
+ * a single field's own template string (title and description are tracked independently). */
1579
+ const getCarouselFieldLocalVarOrdinal = (compositeId, fieldTemplateStr) => {
1580
+ const segments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
1581
+ let varOrdinal = 0;
1582
+ for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
1583
+ if (rcsVarTestRegex.test(segments[segmentIndex])) {
1584
+ if (`${segments[segmentIndex]}_${segmentIndex}` === compositeId) return varOrdinal;
1585
+ varOrdinal += 1;
1586
+ }
1587
+ }
1588
+ return null;
1589
+ };
716
1590
 
717
- const onTagSelectFallback = (data) => {
718
- const tempMsg = `${fallbackMessage}{{${data}}}`;
719
- const error = fallbackMessageErrorHandler(tempMsg);
720
- setFallbackMessage(tempMsg);
721
- setFallbackMessageError(error);
1591
+ // The card's placeholder (numeric {{N}} or a bare semantic token like {{first_name}}) is locked
1592
+ // here picking a tag only supplies/replaces the *value* behind that slot in cardVarMapped, it
1593
+ // never rewrites the card's title/description text. `focusedVarId` is passed in by the caller
1594
+ // (title vs description each track their own last-focused slot, like titleTextAreaId/
1595
+ // descTextAreaId) so a click on one field's "Add Labels" can never apply to the other field's
1596
+ // last-focused slot. The slot's key is its global position across the whole carousel — assigned
1597
+ // once from where its token sits in the template text, never from whatever tag currently fills
1598
+ // it — so swapping tags never changes the key and never collides with another card's slot.
1599
+ const onCarouselTagSelect = (data, focusedVarId, isDescField) => {
1600
+ if (!focusedVarId) return;
1601
+ const activeIdx = parseInt(activeCarouselIndex, 10);
1602
+ const cardIndex = isNaN(activeIdx) ? 0 : activeIdx;
1603
+ const fieldTemplateStr = (isDescField
1604
+ ? carouselData?.[cardIndex]?.description
1605
+ : carouselData?.[cardIndex]?.title) || '';
1606
+ const localVarOrdinal = getCarouselFieldLocalVarOrdinal(focusedVarId, fieldTemplateStr);
1607
+ if (localVarOrdinal == null) return;
1608
+ const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
1609
+ const slotKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
1610
+ if (!slotKey) return;
1611
+
1612
+ setCardVarMapped((prev) => ({
1613
+ ...(prev || {}),
1614
+ [slotKey]: `{{${data}}}`,
1615
+ }));
722
1616
  };
723
1617
 
1618
+ const onCarouselTitleTagSelect = (data) => onCarouselTagSelect(data, carouselTitleFocusedVarId, false);
1619
+
1620
+ const onCarouselDescTagSelect = (data) => onCarouselTagSelect(data, carouselDescFocusedVarId, true);
724
1621
 
725
1622
  //removing optout tag for rcs
726
1623
  const getRcsTags = () => {
@@ -733,15 +1630,14 @@ export const Rcs = (props) => {
733
1630
  };
734
1631
  // tag Code end
735
1632
 
736
- const renderLabel = (value, showLabel, desc) => {
737
- const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
1633
+ const renderLabel = (value, desc) => {
738
1634
  return (
739
1635
  <>
740
- <RcsLabel>
1636
+ <div className="rcs-form-section-heading">
741
1637
  <CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
742
- </RcsLabel>
1638
+ </div>
743
1639
  {desc && (
744
- <CapLabel type="label3" style={{ marginBottom: '17px' }}>
1640
+ <CapLabel type="label3" className="rcs-form-field-caption">
745
1641
  {formatMessage(messages[desc])}
746
1642
  </CapLabel>
747
1643
  )}
@@ -760,13 +1656,8 @@ export const Rcs = (props) => {
760
1656
  },
761
1657
  {
762
1658
  value: contentType.carousel,
763
- label: (
764
- <CapTooltip title={formatMessage(messages.disabledCarouselTooltip)}>
765
- {formatMessage(messages.carousel)}
766
- </CapTooltip>
767
- ),
768
- disabled: true,
769
- },
1659
+ label: formatMessage(messages.carousel),
1660
+ }
770
1661
  ];
771
1662
 
772
1663
  const onTemplateNameChange = ({ target: { value } }) => {
@@ -777,6 +1668,10 @@ export const Rcs = (props) => {
777
1668
 
778
1669
  const onTemplateTypeChange = ({ target: { value } }) => {
779
1670
  setTemplateType(value);
1671
+ // Carousel has per-card media; keep template-level media type neutral.
1672
+ if (value === contentType.carousel) {
1673
+ setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
1674
+ }
780
1675
  };
781
1676
 
782
1677
 
@@ -797,8 +1692,39 @@ export const Rcs = (props) => {
797
1692
  const onTemplateMediaTypeChange = ({ target: { value } }) => {
798
1693
  setTemplateMediaType(value);
799
1694
  };
800
-
801
-
1695
+ const renderedRCSEditMessage = (descArray, type) => {
1696
+ const renderArray = [];
1697
+ if (descArray?.length) {
1698
+ descArray.forEach((elem, index) => {
1699
+ if (rcsVarTestRegex.test(elem)) {
1700
+ // Variable input
1701
+ renderArray.push(
1702
+ <TextArea
1703
+ id={`${elem}_${index}`}
1704
+ key={`${elem}_${index}`}
1705
+ placeholder={`enter the value for ${elem}`}
1706
+ autosize={{ minRows: 1, maxRows: 3 }}
1707
+ onChange={e => textAreaValueChange(e)}
1708
+ value={textAreaValue(index, type)}
1709
+ onFocus={(e) => setTextAreaId(e, type)}
1710
+ />
1711
+ );
1712
+ } else if (elem) {
1713
+ // Static text
1714
+ renderArray.push(
1715
+ <TextArea
1716
+ key={`static_${index}`}
1717
+ value={elem}
1718
+ autosize={{ minRows: 1, maxRows: 3 }}
1719
+ disabled
1720
+ className="rcs-edit-template-message-static-textarea"
1721
+ />
1722
+ );
1723
+ }
1724
+ });
1725
+ }
1726
+ return renderArray;
1727
+ };
802
1728
  const onTemplateTitleChange = ({ target: { value } }) => {
803
1729
  let errorMessage = false;
804
1730
  if (templateType === contentType.rich_card && !value.trim()) {
@@ -814,7 +1740,7 @@ export const Rcs = (props) => {
814
1740
 
815
1741
  const onTemplateDescChange = ({ target: { value } }) => {
816
1742
  let errorMessage = false;
817
- if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
1743
+ if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
818
1744
  errorMessage = formatMessage(messages.templateMessageLengthError);
819
1745
  } else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
820
1746
  errorMessage = formatMessage(messages.templateMessageLengthError);
@@ -830,16 +1756,16 @@ export const Rcs = (props) => {
830
1756
 
831
1757
  const templateDescErrorHandler = (value) => {
832
1758
  let errorMessage = false;
833
- const { isBraceError } = validateTags({
1759
+ const { unsupportedTags, isBraceError } = validateTags({
834
1760
  content: value,
835
1761
  tagsParam: tags,
1762
+ injectedTagsParams: injectedTags,
836
1763
  location,
837
1764
  tagModule: getDefaultTags,
838
- isFullMode,
839
1765
  }) || {};
840
1766
 
841
1767
  const maxLength = templateType === contentType.text_message
842
- ? RCS_TEXT_MESSAGE_MAX_LENGTH
1768
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
843
1769
  : RCS_RICH_CARD_MAX_LENGTH;
844
1770
 
845
1771
  if (value === '' && isMediaTypeText) {
@@ -855,19 +1781,6 @@ export const Rcs = (props) => {
855
1781
  };
856
1782
 
857
1783
 
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
1784
  // Check for forbidden characters: square brackets [] and single curly braces {}
872
1785
  const forbiddenCharactersValidation = (value) => {
873
1786
  if (!value) return false;
@@ -912,53 +1825,43 @@ export const Rcs = (props) => {
912
1825
  if(!isFullMode){
913
1826
  return false;
914
1827
  }
915
- if (!/^\w+$/.test(paramName)) {
1828
+ // Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
1829
+ if (!/^[\w.]+$/.test(paramName)) {
916
1830
  return formatMessage(messages.unknownCharactersError);
917
1831
  }
918
1832
  }
919
1833
  return false;
920
1834
  };
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
1835
 
949
1836
  const onMessageAddVar = () => {
950
- onAddVar(MESSAGE_TEXT, templateDesc, rcsVarRegex);
1837
+ onAddVar(templateDesc);
951
1838
  };
952
1839
 
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));
1840
+ /**
1841
+ * Returns the smallest positive integer not already used as a `{{N}}` variable
1842
+ * in either the title or description fields, or null if the limit (19) is reached.
1843
+ * Scans both fields so title and description vars never share the same number
1844
+ * (duplicate numbers would share a cardVarMapped key and bleed values across fields).
1845
+ */
1846
+ const getNextRcsNumericVarNumber = (titleStr, descStr) => {
1847
+ const allExistingVars = [
1848
+ ...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
1849
+ ...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
1850
+ ];
1851
+ const existingNumbers = allExistingVars.flatMap(varToken => {
1852
+ const numericMatch = varToken.match(/\d+/);
1853
+ return numericMatch ? [parseInt(numericMatch[0], 10)] : [];
1854
+ });
957
1855
  let nextNumber = 1;
958
1856
  while (existingNumbers.includes(nextNumber)) {
959
1857
  nextNumber++;
960
1858
  }
961
- if (nextNumber > 19) {
1859
+ return nextNumber > 19 ? null : nextNumber;
1860
+ };
1861
+
1862
+ const onAddVar = (messageContent) => {
1863
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
1864
+ if (nextNumber === null) {
962
1865
  return;
963
1866
  }
964
1867
  const nextVar = `{{${nextNumber}}}`;
@@ -970,15 +1873,13 @@ const onAddVar = (type, messageContent, regex) => {
970
1873
  };
971
1874
 
972
1875
  const onTitleAddVar = () => {
973
- // Always append the next variable at the end, like WhatsApp
1876
+ // Scan both title AND description so the new title var number doesn't
1877
+ // duplicate a number already used in the description. Duplicate numeric
1878
+ // names would share the same cardVarMapped semantic key, causing the
1879
+ // description slot to reflect the title slot value and vice-versa.
974
1880
  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) {
1881
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
1882
+ if (nextNumber === null) {
982
1883
  return;
983
1884
  }
984
1885
  const nextVar = `{{${nextNumber}}}`;
@@ -990,26 +1891,17 @@ const onTitleAddVar = () => {
990
1891
  setTemplateTitleError(error);
991
1892
  };
992
1893
 
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
- };
1894
+ const appendVarToCarouselField = (cardIndex, fieldName) => {
1895
+ const token = getNextCarouselVarToken(carouselData);
1896
+ if (!token) return;
1897
+ setCarouselData((prev = []) => {
1898
+ const updated = cloneDeep(prev);
1899
+ if (!updated[cardIndex]) return prev;
1900
+ const current = (updated[cardIndex][fieldName] || '').toString();
1901
+ updated[cardIndex][fieldName] = `${current}${token}`;
1902
+ return updated;
1903
+ });
1904
+ };
1013
1905
 
1014
1906
  const textAreaValue = (idValue, type) => {
1015
1907
  if (idValue >= 0) {
@@ -1025,7 +1917,60 @@ const splitTemplateVarString = (str) => {
1025
1917
  return "";
1026
1918
  };
1027
1919
 
1028
- const textAreaValueChange = (e, type) => {
1920
+ // Carousel: render variable-value editor for a given template string (title/description) belonging
1921
+ // to `cardIndex`. This matches rich-card/text edit behavior: static pieces are read-only, variable
1922
+ // tokens are editable. Each slot's key comes from `computeCarouselVarSlotKeys` — stable regardless
1923
+ // of which tag fills it, and independent even from another occurrence of the same tag name.
1924
+ const renderCarouselEditMessage = (templateStr, cardIndex, fieldType) => {
1925
+ const renderArray = [];
1926
+ const templateArr = splitTemplateVarString(templateStr);
1927
+ const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
1928
+ const slotKeysForCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
1929
+ const fieldKeys = (isDescField ? slotKeysForCard?.description : slotKeysForCard?.title) ?? [];
1930
+ let varOrdinal = 0;
1931
+ if (templateArr?.length) {
1932
+ templateArr.forEach((elem, index) => {
1933
+ if (rcsVarTestRegex.test(elem)) {
1934
+ const varName = getVarNameFromToken(elem);
1935
+ const mapKey = fieldKeys[varOrdinal];
1936
+ varOrdinal += 1;
1937
+ const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
1938
+ renderArray.push(
1939
+ <div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
1940
+ <TextArea
1941
+ id={`${elem}_${index}`}
1942
+ placeholder={`enter the value for ${elem}`}
1943
+ autosize={{ minRows: 1, maxRows: 3 }}
1944
+ onChange={(e) => carouselTextAreaValueChange(e, cardIndex, fieldType)}
1945
+ value={computedValue}
1946
+ onFocus={(e) => {
1947
+ const id = e?.target?.id || e?.currentTarget?.id || '';
1948
+ if (fieldType === RCS_TAG_AREA_FIELD_DESC) {
1949
+ setCarouselDescFocusedVarId(id);
1950
+ } else {
1951
+ setCarouselTitleFocusedVarId(id);
1952
+ }
1953
+ }}
1954
+ />
1955
+ </div>
1956
+ );
1957
+ } else if (elem) {
1958
+ renderArray.push(
1959
+ <CapHeading
1960
+ key={`static_${index}`}
1961
+ type="h4"
1962
+ className="rcs-edit-template-message-split"
1963
+ >
1964
+ {elem}
1965
+ </CapHeading>
1966
+ );
1967
+ }
1968
+ });
1969
+ }
1970
+ return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
1971
+ };
1972
+
1973
+ const textAreaValueChange = (e) => {
1029
1974
  const value = e?.target?.value ?? '';
1030
1975
  const id = e?.target?.id || e?.currentTarget?.id || '';
1031
1976
  if (!id) return;
@@ -1036,16 +1981,43 @@ const splitTemplateVarString = (str) => {
1036
1981
  const variableName = getVarNameFromToken(token);
1037
1982
 
1038
1983
  if (variableName) {
1984
+ const nextVal = isInvalidValue ? "" : value;
1039
1985
  setCardVarMapped((prev) => ({
1040
1986
  ...prev,
1041
- [variableName]: isInvalidValue ? "" : value,
1987
+ [variableName]: nextVal,
1042
1988
  }));
1043
1989
  }
1044
1990
  };
1045
1991
 
1046
- const setTextAreaId = (e, type) => {
1992
+ // Carousel: typing directly into a slot's value box (as opposed to picking a tag) same global
1993
+ // positional key as onCarouselTagSelect/renderCarouselEditMessage, so both ways of filling a
1994
+ // slot land on the identical key.
1995
+ const carouselTextAreaValueChange = (e, cardIndex, fieldType) => {
1996
+ const value = e?.target?.value ?? '';
1047
1997
  const id = e?.target?.id || e?.currentTarget?.id || '';
1048
1998
  if (!id) return;
1999
+ const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
2000
+ const fieldTemplateStr = (isDescField
2001
+ ? carouselData?.[cardIndex]?.description
2002
+ : carouselData?.[cardIndex]?.title) || '';
2003
+ const localVarOrdinal = getCarouselFieldLocalVarOrdinal(id, fieldTemplateStr);
2004
+ if (localVarOrdinal == null) return;
2005
+ const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
2006
+ const mapKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
2007
+ if (!mapKey) return;
2008
+ const isInvalidValue = value?.trim() === "";
2009
+ const nextVal = isInvalidValue ? "" : value;
2010
+ setCardVarMapped((prev) => ({
2011
+ ...prev,
2012
+ [mapKey]: nextVal,
2013
+ }));
2014
+ };
2015
+
2016
+ const setTextAreaId = (e, type) => {
2017
+ // VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
2018
+ // have an `.target.id` shape. Support both.
2019
+ const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
2020
+ if (!id) return;
1049
2021
  if (type === TITLE_TEXT) setTitleTextAreaId(id);
1050
2022
  else setDescTextAreaId(id);
1051
2023
  };
@@ -1063,10 +2035,12 @@ const splitTemplateVarString = (str) => {
1063
2035
  </CapRow>
1064
2036
  }
1065
2037
  description={
1066
- <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
2038
+ !isEditFlow && (
2039
+ <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
2040
+ )
1067
2041
  }
1068
2042
  />
1069
-
2043
+
1070
2044
  <CapActionButton
1071
2045
  buttonType={buttonType}
1072
2046
  updateButtonChange={updateButtonChange}
@@ -1074,45 +2048,86 @@ const splitTemplateVarString = (str) => {
1074
2048
  suggestions={suggestions}
1075
2049
  isEditFlow={isEditFlow}
1076
2050
  isFullMode={isFullMode}
1077
- maxButtons={MAX_BUTTONS}
1078
- />
1079
- </>
1080
- );
1081
- };
1082
-
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
- );
2051
+ maxButtons={MAX_BUTTONS}
2052
+ host={hostName}
2053
+ />
2054
+ </>
2055
+ );
2056
+ };
2057
+
2058
+ const getRcsValueMap = (fieldTemplateString, fieldType) => {
2059
+ if (!fieldTemplateString) return {};
2060
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
2061
+ const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
2062
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
2063
+ const segmentIdToResolvedValue = {};
2064
+ let varOrdinal = 0;
2065
+ templateSegments.forEach((segmentToken, segmentIndexInField) => {
2066
+ if (rcsVarTestRegex.test(segmentToken)) {
2067
+ const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
2068
+ const varName = getVarNameFromToken(segmentToken);
2069
+ const globalSlot = slotOffset + varOrdinal;
2070
+ varOrdinal += 1;
2071
+ segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
2072
+ cardVarMapped,
2073
+ varName,
2074
+ globalSlot,
2075
+ isEditLike,
2076
+ rcsSpanningSemanticVarNames.has(varName),
2077
+ );
2078
+ }
2079
+ });
2080
+ return segmentIdToResolvedValue;
2081
+ };
2082
+
2083
+ const titleVarSegmentValueMapById = useMemo(
2084
+ () => getRcsValueMap(templateTitle, TITLE_TEXT),
2085
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
2086
+ );
2087
+ const descriptionVarSegmentValueMapById = useMemo(
2088
+ () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
2089
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
2090
+ );
2091
+
2092
+ const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
2093
+ const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
2094
+ if (underscoreIndexInCompositeId === -1) return;
2095
+ const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
2096
+ const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
2097
+ if (variableName === undefined || variableName === null || variableName === '') return;
2098
+ const isInvalidValue = value?.trim() === '';
2099
+ const coercedSlotValue = isInvalidValue ? '' : value;
2100
+ const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
2101
+ const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
2102
+ varSegmentCompositeDomId,
2103
+ templateStringForField,
2104
+ type,
2105
+ );
2106
+ const fieldOffset = type === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
2107
+ setCardVarMapped((previousCardVarMapped) => {
2108
+ const updatedCardVarMapped = { ...previousCardVarMapped };
2109
+ // Remove stale semantic key: keeping it causes every other slot sharing the same
2110
+ // variable name (e.g. {{adv}} in both title and description) to read the same value
2111
+ // via the semantic-key fallback in resolveCardVarMappedSlotValue.
2112
+ delete updatedCardVarMapped[variableName];
2113
+ if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
2114
+ updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
2115
+ // Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
2116
+ const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
2117
+ let varOrdinal = 0;
2118
+ for (let i = 0; i < fieldSegments.length; i++) {
2119
+ if (rcsVarTestRegex.test(fieldSegments[i])) {
2120
+ const slotVarName = getVarNameFromToken(fieldSegments[i]);
2121
+ const globalSlot = fieldOffset + varOrdinal;
2122
+ varOrdinal += 1;
2123
+ if (slotVarName === variableName && globalSlot !== globalVarSlotIndexZeroBased) {
2124
+ updatedCardVarMapped[String(globalSlot + 1)] = coercedSlotValue;
2125
+ }
2126
+ }
1112
2127
  }
1113
- });
1114
- }
1115
- return renderArray;
2128
+ }
2129
+ return updatedCardVarMapped;
2130
+ });
1116
2131
  };
1117
2132
 
1118
2133
  const renderTextComponent = () => {
@@ -1132,6 +2147,7 @@ const splitTemplateVarString = (str) => {
1132
2147
  }
1133
2148
  suffix={
1134
2149
  <>
2150
+
1135
2151
  {(isEditFlow || !isFullMode) ? (
1136
2152
  <TagList
1137
2153
  label={formatMessage(globalMessages.addLabels)}
@@ -1148,18 +2164,28 @@ const splitTemplateVarString = (str) => {
1148
2164
  type="flat"
1149
2165
  isAddBtn
1150
2166
  onClick={onTitleAddVar}
1151
- disabled={!!templateTitleError}
2167
+ disabled={templateTitleError}
1152
2168
  >
1153
2169
  {formatMessage(messages.addVar)}
1154
2170
  </CapButton>
1155
- )}
2171
+ )}
1156
2172
  </>
1157
- }
2173
+ }
2174
+ />
2175
+
2176
+ {(isEditFlow || !isFullMode) ? (
2177
+ <VarSegmentMessageEditor
2178
+ key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
2179
+ templateString={templateTitle}
2180
+ valueMap={titleVarSegmentValueMapById}
2181
+ onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
2182
+ onFocus={(id) => setTitleTextAreaId(id)}
2183
+ varRegex={rcsVarRegex}
2184
+ placeholderPrefix=""
2185
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
1158
2186
  />
1159
- <div className="rcs_text_area_wrapper">
1160
- {(isEditFlow || !isFullMode) ? (
1161
- renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
1162
2187
  ) : (
2188
+ <div className="rcs_text_area_wrapper">
1163
2189
  <CapInput
1164
2190
  className={`rcs-template-title-input ${
1165
2191
  !isTemplateApproved ? "rcs-edit-disabled" : ""
@@ -1172,8 +2198,8 @@ const splitTemplateVarString = (str) => {
1172
2198
  errorMessage={templateTitleError}
1173
2199
  disabled={isEditFlow || !isFullMode}
1174
2200
  />
2201
+ </div>
1175
2202
  )}
1176
- </div>
1177
2203
  {(isEditFlow || !isFullMode) && templateTitleError && (
1178
2204
  <CapError className="rcs-template-title-error">
1179
2205
  {templateTitleError}
@@ -1181,7 +2207,7 @@ const splitTemplateVarString = (str) => {
1181
2207
  )}
1182
2208
  {!isEditFlow && isFullMode && renderTitleCharacterCount()}
1183
2209
  </>
1184
- )}
2210
+ )}
1185
2211
 
1186
2212
  {/* Template Message */}
1187
2213
  <CapRow useLegacy id="rcs-template-message-label">
@@ -1219,9 +2245,21 @@ const splitTemplateVarString = (str) => {
1219
2245
  />
1220
2246
  </CapRow>
1221
2247
  <CapRow useLegacy className="rcs-create-template-message-input">
1222
- <div className="rcs_text_area_wrapper">
1223
- {(isEditFlow || !isFullMode)
1224
- ? renderedRCSEditMessage(splitTemplateVarString(templateDesc), MESSAGE_TEXT)
2248
+ {/* 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. */}
2249
+ <CapRow className="rcs_text_area_wrapper">
2250
+ {(isEditFlow || !isFullMode)?
2251
+ (
2252
+ <VarSegmentMessageEditor
2253
+ key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
2254
+ templateString={templateDesc}
2255
+ valueMap={descriptionVarSegmentValueMapById}
2256
+ onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
2257
+ onFocus={(id) => setDescTextAreaId(id)}
2258
+ varRegex={rcsVarRegex}
2259
+ placeholderPrefix=""
2260
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
2261
+ />
2262
+ )
1225
2263
  : (
1226
2264
  <>
1227
2265
  <TextArea
@@ -1259,13 +2297,15 @@ const splitTemplateVarString = (str) => {
1259
2297
  </>
1260
2298
  )
1261
2299
  }
1262
- </div>
2300
+ </CapRow>
1263
2301
  {(isEditFlow || !isFullMode) && templateDescError && (
1264
2302
  <CapError className="rcs-template-message-error">
1265
2303
  {templateDescError}
1266
2304
  </CapError>
1267
2305
  )}
1268
- {!isEditFlow && isFullMode && renderDescriptionCharacterCount()}
2306
+ {(isEditFlow || !isFullMode)
2307
+ ? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
2308
+ : (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
1269
2309
  {!isFullMode && hasTag() && (
1270
2310
  <CapAlert
1271
2311
  message={
@@ -1279,24 +2319,13 @@ const splitTemplateVarString = (str) => {
1279
2319
  />
1280
2320
  )}
1281
2321
  </CapRow>
1282
- {renderButtonComponent()}
2322
+ {((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
2323
+ renderButtonComponent()}
1283
2324
  </>
1284
2325
 
1285
2326
  );
1286
2327
  };
1287
2328
 
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
2329
  // Get character count for title (rich card only)
1301
2330
  const getTitleCharacterCount = () => {
1302
2331
  if (templateType === contentType.text_message) return 0;
@@ -1311,7 +2340,7 @@ const splitTemplateVarString = (str) => {
1311
2340
  // Get max length for description based on template type
1312
2341
  const getDescriptionMaxLength = () => {
1313
2342
  return templateType === contentType.text_message
1314
- ? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
2343
+ ? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
1315
2344
  : RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
1316
2345
  };
1317
2346
 
@@ -1374,287 +2403,34 @@ const splitTemplateVarString = (str) => {
1374
2403
  return false;
1375
2404
  };
1376
2405
 
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
- };
2406
+ const renderFallBackSmsComponent = () => (
2407
+ <SmsFallback
2408
+ value={smsFallbackData}
2409
+ onChange={setSmsFallbackData}
2410
+ parentLocation={location}
2411
+ smsRegister={smsRegister}
2412
+ isFullMode={isFullMode}
2413
+ selectedOfferDetails={selectedOfferDetails}
2414
+ channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
2415
+ sectionTitle={
2416
+ smsFallbackData
2417
+ ? formatMessage(messages.fallbackLabel)
2418
+ : formatMessage(messages.smsFallbackOptional)
2419
+ }
2420
+ templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
2421
+ templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
2422
+ /* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
2423
+ showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
2424
+ disableSelectTemplate={isEditFlow}
2425
+ eventContextTags={eventContextTags}
2426
+ onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
2427
+ isRcsEditFlow={isEditFlow}
2428
+ />
2429
+ );
1654
2430
 
1655
2431
  const uploadRcsImage = useCallback((file, type, fileParams, index) => {
1656
2432
  setImageError(null);
1657
- const isRcsThumbnail = index === 1;
2433
+ const isRcsThumbnail = isThumbnailAssetIndex(index);
1658
2434
  actions.uploadRcsAsset(file, type, {
1659
2435
  isRcsThumbnail,
1660
2436
  ...fileParams,
@@ -1692,21 +2468,15 @@ const splitTemplateVarString = (str) => {
1692
2468
  const updateOnRcsImageReUpload = useCallback(() => {
1693
2469
  setUpdateRcsImageSrc('');
1694
2470
  }, []);
1695
-
1696
-
1697
- const uploadRcsVideo = (file, type, fileParams) => {
1698
- setImageError(null);
2471
+ const uploadRcsVideo = (file, type, fileParams, index) => {
1699
2472
  actions.uploadRcsAsset(file, type, {
1700
2473
  ...fileParams,
1701
2474
  type: 'video',
1702
2475
  channel: RCS,
1703
2476
  isGenerateVideoThumbnail: false,
1704
- });
2477
+ }, index);
1705
2478
  };
1706
2479
 
1707
- const updateRcsVideoSrc = (val) => {
1708
- setRcsVideoSrc(val);
1709
- };
1710
2480
  const setUpdateRcsVideoSrc = useCallback((index, val) => {
1711
2481
  setRcsVideoSrc(val);
1712
2482
  setAssetList(val);
@@ -1729,13 +2499,13 @@ const splitTemplateVarString = (str) => {
1729
2499
  updateRcsThumbnailSrc('');
1730
2500
  };
1731
2501
 
1732
- const renderThumbnailComponent = () => {
2502
+ const renderThumbnailComponent = () => {
1733
2503
  const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
1734
2504
  return !isEditFlow && (
1735
2505
  <>
1736
2506
  <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
1737
2507
  <CapImageUpload
1738
- style={{ paddingTop: '20px' }}
2508
+ className="rcs-image-upload-with-top-padding"
1739
2509
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1740
2510
  imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
1741
2511
  imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
@@ -1753,6 +2523,7 @@ const splitTemplateVarString = (str) => {
1753
2523
  channel={RCS}
1754
2524
  channelSpecificStyle={!isFullMode}
1755
2525
  skipDimensionValidation={true}
2526
+ showReUploadButton={!isEditFlow && isFullMode}
1756
2527
  />
1757
2528
  </>
1758
2529
  )
@@ -1778,7 +2549,7 @@ const splitTemplateVarString = (str) => {
1778
2549
  value: dim.type,
1779
2550
  label: `${dim.label}`
1780
2551
  }))}
1781
- style={{ marginBottom: '20px' }}
2552
+ className="rcs-dimension-select--bottom-spacing"
1782
2553
  />
1783
2554
  </>
1784
2555
  )}
@@ -1792,7 +2563,7 @@ const splitTemplateVarString = (str) => {
1792
2563
  </div>
1793
2564
  ) : (
1794
2565
  <CapImageUpload
1795
- style={{ paddingTop: '20px' }}
2566
+ className="rcs-image-upload-with-top-padding"
1796
2567
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1797
2568
  imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
1798
2569
  imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
@@ -1803,7 +2574,7 @@ const splitTemplateVarString = (str) => {
1803
2574
  updateImageSrc={setUpdateRcsImageSrc}
1804
2575
  updateOnReUpload={updateOnRcsImageReUpload}
1805
2576
  index={0}
1806
- className="cap-custom-image-upload"
2577
+ className="cap-custom-image-upload rcs-image-upload--top-spacing"
1807
2578
  key={`rcs-uploaded-image-${selectedDimension}`}
1808
2579
  imageData={rcsData}
1809
2580
  channel={RCS}
@@ -1814,7 +2585,7 @@ const splitTemplateVarString = (str) => {
1814
2585
 
1815
2586
  </>
1816
2587
  );
1817
- }
2588
+ }
1818
2589
 
1819
2590
  const renderVideoComponent = () => {
1820
2591
  const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
@@ -1833,7 +2604,7 @@ const splitTemplateVarString = (str) => {
1833
2604
  value: dim.type,
1834
2605
  label: `${dim.label}`
1835
2606
  }))}
1836
- style={{ marginBottom: '20px' }}
2607
+ className="rcs-dimension-select--bottom-spacing"
1837
2608
  />
1838
2609
  )}
1839
2610
  {(isEditFlow || !isFullMode) ? (
@@ -1891,10 +2662,48 @@ const splitTemplateVarString = (str) => {
1891
2662
  };
1892
2663
 
1893
2664
  const getRcsPreview = () => {
2665
+
2666
+ if (templateType === contentType.carousel) {
2667
+ const cardsForPreview = buildCarouselCardsForPreview(carouselData);
2668
+ const carouselDimKey = getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth);
2669
+ const carouselImgDims =
2670
+ RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
2671
+ const carouselVidDims =
2672
+ RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
2673
+ || RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
2674
+ // Debug log for embedded/library mode preview payload (carousel)
2675
+ // eslint-disable-next-line no-console
2676
+ return (
2677
+ <UnifiedPreview
2678
+ channel={RCS}
2679
+ content={{
2680
+ carouselData: cardsForPreview,
2681
+ carouselPreviewDimensions: {
2682
+ imageWidth: carouselImgDims.width,
2683
+ imageHeight: carouselImgDims.height,
2684
+ videoThumbWidth: carouselVidDims.width,
2685
+ videoThumbHeight: carouselVidDims.height,
2686
+ },
2687
+ }}
2688
+ device={ANDROID}
2689
+ showDeviceToggle={false}
2690
+ showHeader={false}
2691
+ formatMessage={formatMessage}
2692
+ />
2693
+ );
2694
+ }
1894
2695
 
1895
2696
  const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
1896
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1897
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
2697
+ const isSlotMappingMode = isEditFlow || !isFullMode;
2698
+ const titleVarCountForResolve = isMediaTypeText
2699
+ ? 0
2700
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
2701
+ const resolvedTitle = isMediaTypeText
2702
+ ? ''
2703
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
2704
+ const resolvedDesc = isSlotMappingMode
2705
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
2706
+ : templateDesc;
1898
2707
  return (
1899
2708
  <UnifiedPreview
1900
2709
  channel={RCS}
@@ -1917,51 +2726,39 @@ const splitTemplateVarString = (str) => {
1917
2726
  );
1918
2727
  };
1919
2728
 
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
2729
  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;
2730
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1961
2731
  const alignment = isMediaTypeImage
1962
2732
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
1963
2733
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
1964
2734
 
2735
+ const heightTypeForCardWidth = isMediaTypeText
2736
+ ? undefined
2737
+ : isMediaTypeImage
2738
+ ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
2739
+ : isMediaTypeVideo
2740
+ ? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
2741
+ : undefined;
2742
+ const cardWidthFromSelection =
2743
+ heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
2744
+
2745
+ /** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
2746
+ const smsFallbackPayloadContent = buildSmsFallBackContentForPayload({
2747
+ smsFallbackData,
2748
+ templateData,
2749
+ isFullMode,
2750
+ smsRegister,
2751
+ });
2752
+
2753
+ const carouselCardContent = isCarouselType
2754
+ ? buildCarouselCardContentForPayload(carouselData, {
2755
+ isSlotMappingMode,
2756
+ cardVarMapped,
2757
+ selectedCarouselHeight,
2758
+ rcsVarRegex,
2759
+ })
2760
+ : null;
2761
+
1965
2762
  const payload = {
1966
2763
  name: templateName,
1967
2764
  versions: {
@@ -1970,16 +2767,22 @@ const splitTemplateVarString = (str) => {
1970
2767
  RCS: {
1971
2768
  rcsContent: {
1972
2769
  ...(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: [
2770
+ cardType: isCarouselType ? contentType.carousel : STANDALONE,
2771
+ cardSettings: isCarouselType
2772
+ ? { cardOrientation: VERTICAL, cardWidth: selectedCarouselWidth || SMALL }
2773
+ : {
2774
+ cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
2775
+ ...(alignment && { mediaAlignment: alignment }),
2776
+ cardWidth: SMALL,
2777
+ },
2778
+ cardContent: isCarouselType
2779
+ ? carouselCardContent
2780
+ : [
1980
2781
  {
1981
- title: resolvedTitle,
1982
- description: resolvedDesc,
2782
+ // Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
2783
+ // / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
2784
+ title: templateTitle,
2785
+ description: templateDesc,
1983
2786
  mediaType: templateMediaType,
1984
2787
  ...(!isMediaTypeText && {media: {
1985
2788
  mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
@@ -1988,23 +2791,32 @@ const splitTemplateVarString = (str) => {
1988
2791
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
1989
2792
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
1990
2793
  }}),
1991
- ...(!isFullMode && (() => {
1992
- const tokens = [
1993
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
1994
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
2794
+ ...(isSlotMappingMode && (() => {
2795
+ const templateVarTokens = [
2796
+ ...(templateTitle?.match(rcsVarRegex) ?? []),
2797
+ ...(templateDesc?.match(rcsVarRegex) ?? []),
1995
2798
  ];
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
- }
2799
+ const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
2800
+ cardVarMapped,
2801
+ );
2802
+ // Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
2803
+ // semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
2804
+ // and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
2805
+ const persistedSlotVarMap = {};
2806
+ templateVarTokens.forEach((token, slotIndexZeroBased) => {
2807
+ const varName = getVarNameFromToken(token);
2808
+ if (!varName) return;
2809
+ const resolvedRawValue = resolveCardVarMappedSlotValue(
2810
+ cardVarMappedForRcsCardOnly,
2811
+ varName,
2812
+ slotIndexZeroBased,
2813
+ isSlotMappingMode,
2814
+ rcsSpanningSemanticVarNames.has(varName),
2815
+ );
2816
+ const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
2817
+ persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
2006
2818
  });
2007
- return { cardVarMapped: nextMap };
2819
+ return { cardVarMapped: persistedSlotVarMap };
2008
2820
  })()),
2009
2821
  ...(suggestions.length > 0 && { suggestions }),
2010
2822
  }
@@ -2012,17 +2824,7 @@ const splitTemplateVarString = (str) => {
2012
2824
  contentType: isFullMode ? templateType : RICHCARD,
2013
2825
  ...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
2014
2826
  },
2015
- smsFallBackContent: {
2016
- message: fallbackMessage,
2017
- ...(isDltEnabled && {
2018
- templateConfigs: {
2019
- templateId,
2020
- templateName: template_name,
2021
- template,
2022
- registeredSenderIds,
2023
- },
2024
- }),
2025
- },
2827
+ ...smsFallbackPayloadContent,
2026
2828
  },
2027
2829
  },
2028
2830
  },
@@ -2032,6 +2834,126 @@ const splitTemplateVarString = (str) => {
2032
2834
  return payload;
2033
2835
  };
2034
2836
 
2837
+ /** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
2838
+ const testPreviewFormData = useMemo(() => {
2839
+ const payload = createPayload();
2840
+ const rcs = payload?.versions?.base?.content?.RCS;
2841
+ if (!rcs) return null;
2842
+ // createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
2843
+ const accountIdForCreateMessageMeta =
2844
+ (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
2845
+ ? String(wecrmAccountId)
2846
+ : accountId;
2847
+ const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
2848
+ let rcsForTest = {
2849
+ ...rcs,
2850
+ rcsContent: {
2851
+ ...rcs.rcsContent,
2852
+ ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
2853
+ },
2854
+ };
2855
+ /** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
2856
+ const cardContent = rcsForTest.rcsContent?.cardContent;
2857
+ if (Array.isArray(cardContent) && cardContent[0]) {
2858
+ if (isCarouselType) {
2859
+ // Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
2860
+ // user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
2861
+ // - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
2862
+ // - prepareTagExtractionPayload can extract tag metadata from the card content.
2863
+ // Each variable slot's key comes from computeCarouselVarSlotKeys, so two cards' variables
2864
+ // never share a key even when they pick the same tag name for their own slot.
2865
+ rcsForTest = {
2866
+ ...rcsForTest,
2867
+ rcsContent: {
2868
+ ...rcsForTest.rcsContent,
2869
+ cardContent: cardContent.map((card) => {
2870
+ const rawTitle = card.title || '';
2871
+ const rawDesc = card.description || '';
2872
+ const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex, false);
2873
+ const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex, true);
2874
+ return { ...card, title: resolvedTitle, description: resolvedDesc };
2875
+ }),
2876
+ },
2877
+ };
2878
+ } else if (isSlotMappingModeForPreview) {
2879
+ // Standalone card: coalesce cardVarMapped with the template's slot names so the preview
2880
+ // API receives a correctly-keyed var map for non-carousel templates.
2881
+ const fullCardVarMapped = coalesceCardVarMappedToTemplate(
2882
+ pickRcsCardVarMappedEntries(cardVarMapped),
2883
+ templateTitle,
2884
+ templateDesc,
2885
+ rcsVarRegex,
2886
+ );
2887
+ rcsForTest = {
2888
+ ...rcsForTest,
2889
+ rcsContent: {
2890
+ ...rcsForTest.rcsContent,
2891
+ cardContent: [
2892
+ { ...cardContent[0], cardVarMapped: fullCardVarMapped },
2893
+ ...cardContent.slice(1),
2894
+ ],
2895
+ },
2896
+ };
2897
+ }
2898
+ }
2899
+ const out = {
2900
+ versions: {
2901
+ base: {
2902
+ content: {
2903
+ RCS: rcsForTest,
2904
+ },
2905
+ },
2906
+ },
2907
+ };
2908
+ // Use the smsFallbackTemplateConfigs already built by createPayload() / buildSmsFallBackContentForPayload —
2909
+ // it merges the API baseline (templateId, registeredSenderIds, templateName, etc.) with local
2910
+ // smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced smsTemplateId
2911
+ // in campaign mode. This also keeps templateName (and all other fields) in sync with the approval
2912
+ // payload automatically.
2913
+ const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
2914
+ if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
2915
+ out.templateConfigs = {
2916
+ ...smsFallbackTcFromPayload,
2917
+ traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
2918
+ };
2919
+ }
2920
+ return out;
2921
+ }, [
2922
+ templateName,
2923
+ templateTitle,
2924
+ templateDesc,
2925
+ templateMediaType,
2926
+ cardVarMapped,
2927
+ carouselData,
2928
+ suggestions,
2929
+ rcsImageSrc,
2930
+ rcsVideoSrc,
2931
+ rcsThumbnailSrc,
2932
+ selectedDimension,
2933
+ smsFallbackData,
2934
+ isFullMode,
2935
+ isEditFlow,
2936
+ templateType,
2937
+ accountId,
2938
+ wecrmAccountId,
2939
+ accessToken,
2940
+ accountName,
2941
+ hostName,
2942
+ smsRegister,
2943
+ ]);
2944
+
2945
+ /**
2946
+ * Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
2947
+ * with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
2948
+ * miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
2949
+ */
2950
+ const librarySmsFallbackMergedForValidation = useMemo(
2951
+ () => mergeSmsFallbackForLibrary(templateData, smsFallbackData, isFullMode),
2952
+ [isFullMode, templateData, smsFallbackData],
2953
+ );
2954
+
2955
+
2956
+
2035
2957
  const actionCallback = ({ errorMessage, resp }, isEdit) => {
2036
2958
  // eslint-disable-next-line no-undef
2037
2959
  const error = errorMessage?.message || errorMessage;
@@ -2061,6 +2983,9 @@ const splitTemplateVarString = (str) => {
2061
2983
  _id: params?.id,
2062
2984
  validity: true,
2063
2985
  type: RCS,
2986
+ // CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
2987
+ // the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
2988
+ closeSlideBoxAfterSubmit: !isFullMode,
2064
2989
  };
2065
2990
  getFormData(formDataParams);
2066
2991
  };
@@ -2074,6 +2999,7 @@ const splitTemplateVarString = (str) => {
2074
2999
  actionCallback({ resp, errorMessage });
2075
3000
  setSpin(false); // Always turn off spinner
2076
3001
  if (!errorMessage) {
3002
+ setTemplateStatus(RCS_STATUSES.pending);
2077
3003
  onCreateComplete();
2078
3004
  }
2079
3005
  });
@@ -2085,6 +3011,72 @@ const splitTemplateVarString = (str) => {
2085
3011
  }
2086
3012
  };
2087
3013
 
3014
+ const smsFallbackBlocksDone = () => checkSmsFallbackBlocksDone({
3015
+ isFullMode,
3016
+ smsRegister,
3017
+ smsFallbackData,
3018
+ templateData,
3019
+ smsFallbackMergedForValidation: librarySmsFallbackMergedForValidation,
3020
+ });
3021
+
3022
+ /**
3023
+ * Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
3024
+ * semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
3025
+ * / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
3026
+ */
3027
+ const isLibraryCampaignCardVarMappingIncomplete = () => {
3028
+ if (isFullMode) return false;
3029
+ const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
3030
+ rcsVarTestRegex.test(elem),
3031
+ );
3032
+ const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
3033
+ rcsVarTestRegex.test(elem),
3034
+ );
3035
+ const orderedVarNames = [
3036
+ ...titleTokens.map((token) => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
3037
+ ...descTokens.map((token) => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
3038
+ ];
3039
+ if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
3040
+ return true;
3041
+ }
3042
+ return orderedVarNames.some((name, globalIdx) => {
3043
+ const slotValue = resolveCardVarMappedSlotValue(
3044
+ cardVarMapped,
3045
+ name,
3046
+ globalIdx,
3047
+ true,
3048
+ rcsSpanningSemanticVarNames.has(name),
3049
+ );
3050
+ const stringValue = slotValue == null ? '' : String(slotValue);
3051
+ return stringValue.trim() === '';
3052
+ });
3053
+ };
3054
+
3055
+ const isCarouselLibraryIncomplete = () => {
3056
+ if (!isCarouselType || isFullMode) return false;
3057
+ if ((carouselErrors || []).some((err) => err?.title || err?.description)) return true;
3058
+ // Block if any card's title or description has no content at all (no static text and no variable tokens).
3059
+ const hasEmptyField = (carouselData || []).some((card) =>
3060
+ ['title', 'description'].some((field) => !(card?.[field] || '').trim())
3061
+ );
3062
+ if (hasEmptyField) return true;
3063
+ const slotKeysByCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex);
3064
+ const unfilledVar = (carouselData || []).some((card, cardIndex) =>
3065
+ ['title', 'description'].some((field) => {
3066
+ const isDescField = field === 'description';
3067
+ const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
3068
+ if (tokens.length === 0) return false;
3069
+ const fieldKeys = (isDescField ? slotKeysByCard[cardIndex]?.description : slotKeysByCard[cardIndex]?.title) ?? [];
3070
+ return tokens.some((token, localVarOrdinal) => {
3071
+ const slotKey = fieldKeys[localVarOrdinal];
3072
+ const slotValue = cardVarMapped?.[slotKey];
3073
+ return slotValue == null || String(slotValue).trim() === '';
3074
+ });
3075
+ })
3076
+ );
3077
+ return unfilledVar;
3078
+ };
3079
+
2088
3080
  const isDisableDone = () => {
2089
3081
  if(isEditFlow){
2090
3082
  return false;
@@ -2095,46 +3087,26 @@ const splitTemplateVarString = (str) => {
2095
3087
  }
2096
3088
  }
2097
3089
 
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
- }
3090
+ if (isCarouselLibraryIncomplete()) {
3091
+ return true;
3092
+ }
2118
3093
 
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
- }
3094
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
3095
+ return true;
2127
3096
  }
2128
3097
 
2129
- if (isMediaTypeText && templateDesc.trim() === '') {
3098
+ if (smsFallbackBlocksDone()) {
2130
3099
  return true;
3100
+ }
2131
3101
 
3102
+ if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
3103
+ return true;
2132
3104
  }
2133
- if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
3105
+ if (!isCarouselType && isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
2134
3106
  return true;
2135
3107
  }
2136
3108
 
2137
- if (isMediaTypeVideo && (rcsVideoSrc.videoSrc === '' || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
3109
+ if (!isCarouselType && isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
2138
3110
  return true;
2139
3111
  }
2140
3112
  if (buttonType.includes(CTA)) {
@@ -2146,72 +3118,54 @@ const splitTemplateVarString = (str) => {
2146
3118
  return true;
2147
3119
  }
2148
3120
  }
2149
- if (templateDescError || templateTitleError || fallbackMessageError) {
3121
+ if (templateDescError || templateTitleError) {
3122
+ return true;
3123
+ }
3124
+ if (isSmsFallbackContentTooLong(smsFallbackData, FALLBACK_MESSAGE_MAX_LENGTH)) {
2150
3125
  return true;
2151
3126
  }
2152
3127
  return false;
2153
3128
  };
2154
3129
 
2155
3130
  const isEditDisableDone = () => {
2156
-
2157
- if (templateStatus !== RCS_STATUSES.approved) {
3131
+ if (isFullMode && !isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
2158
3132
  return true;
2159
3133
  }
2160
3134
 
2161
- if (!isFullMode) {
2162
- if (templateName.trim() === '' || templateNameError) {
2163
- return true;
2164
- }
3135
+ if (isCarouselLibraryIncomplete()) {
3136
+ return true;
2165
3137
  }
2166
- if(!isFullMode){
2167
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2168
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2169
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2170
-
2171
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2172
- return true;
2173
- }
2174
-
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
3138
 
2183
- if (hasEmptyMapping) {
2184
- return true;
2185
- }
3139
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
3140
+ return true;
3141
+ }
2186
3142
 
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
- }
3143
+ if (smsFallbackBlocksDone()) {
3144
+ return true;
2195
3145
  }
2196
- if (isMediaTypeText && templateDesc.trim() === '') {
3146
+
3147
+ if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
2197
3148
  return true;
2198
3149
  }
2199
- if (isMediaTypeImage && rcsImageSrc === '') {
3150
+ if (!isCarouselType && isMediaTypeImage && rcsImageSrc === '') {
2200
3151
  return true;
2201
3152
  }
2202
- if(isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
3153
+ if (!isCarouselType && isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
2203
3154
  return true;
2204
3155
  }
2205
3156
 
2206
3157
  if (buttonType.includes(CTA)) {
2207
- const hasValidButtons = suggestions.every(suggestion =>
3158
+ const hasValidButtons = suggestions.every(suggestion =>
2208
3159
  suggestion.text && suggestion.url && !suggestionError && !forbiddenCharactersValidation(suggestion.text)
2209
3160
  );
2210
3161
  if (!hasValidButtons) {
2211
3162
  return true;
2212
3163
  }
2213
3164
  }
2214
- if (templateTitleError || templateDescError || fallbackMessageError) {
3165
+ if (templateTitleError || templateDescError) {
3166
+ return true;
3167
+ }
3168
+ if (isSmsFallbackContentTooLong(smsFallbackData, FALLBACK_MESSAGE_MAX_LENGTH)) {
2215
3169
  return true;
2216
3170
  }
2217
3171
  return false;
@@ -2261,85 +3215,108 @@ const splitTemplateVarString = (str) => {
2261
3215
  };
2262
3216
 
2263
3217
  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
3218
  return (
2279
3219
  <>
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>
3220
+ {templateStatus !== '' && (
3221
+ <CapRow useLegacy className="template-status-container">
3222
+ <CapColumn span={14}>
3223
+ <CapLabel type="label2">
3224
+ {formatMessage(messages.templateStatusLabel)}
3225
+ </CapLabel>
3226
+
3227
+ {!isHostInfoBip && templateStatus && (
3228
+ <CapAlert
3229
+ message={getTemplateStatusMessage()}
3230
+ type={getTemplateStatusType(templateStatus)}
3231
+ />
3232
+ )}
3233
+ </CapColumn>
3234
+ </CapRow>
2292
3235
  )}
2293
- <CapRow className="cap-rcs-creatives">
3236
+ <CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
2294
3237
  <CapColumn span={14}>
2295
3238
  {/* template name */}
2296
3239
  {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
- />
3240
+ isEditFlow ? (
3241
+ <div className="rcs-creative-name-readonly">
3242
+ <CapHeading type="h4">
3243
+ {formatMessage(globalMessages.creativeNameLabel)}
3244
+ </CapHeading>
3245
+ <CapHeading type="h5" className="rcs-creative-name-value">
3246
+ {templateName || '-'}
3247
+ </CapHeading>
3248
+ </div>
3249
+ ) : (
3250
+ <CapInput
3251
+ id="rcs_template_name_input"
3252
+ data-testid="template_name"
3253
+ onChange={onTemplateNameChange}
3254
+ errorMessage={templateNameError}
3255
+ placeholder={formatMessage(
3256
+ globalMessages.templateNamePlaceholder,
3257
+ )}
3258
+ value={templateName || ''}
3259
+ size="default"
3260
+ label={formatMessage(globalMessages.creativeNameLabel)}
3261
+ disabled={(isEditFlow || !isFullMode)}
3262
+ />
3263
+ )
3264
+ )}
3265
+ {isEditFlow ? (
3266
+ <div className="rcs-creative-name-readonly">
3267
+ <CapHeading type="h4">
3268
+ {formatMessage(messages.templateTypeLabel)}
3269
+ </CapHeading>
3270
+ <CapHeading type="h5" className="rcs-creative-name-value">
3271
+ {TEMPLATE_TYPE_OPTIONS.find((option) => option.value === templateType)?.label || '-'}
3272
+ </CapHeading>
3273
+ </div>
3274
+ ) : (
3275
+ <>
3276
+ {renderLabel('templateTypeLabel')}
3277
+ <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
3278
+ <CapRadioGroup
3279
+ id="select-rcs-template-type"
3280
+ options={TEMPLATE_TYPE_OPTIONS}
3281
+ onChange={onTemplateTypeChange}
3282
+ value={templateType}
3283
+ disabled={!isFullMode}
3284
+ />
3285
+ </ConfigProvider>
3286
+ </>
2310
3287
  )}
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
3288
 
2322
- {/* Show media only for rich_card or carousel */}
2323
- {(templateType === contentType.rich_card || templateType === contentType.carousel) && (
3289
+ {templateType === contentType.carousel ? (
3290
+ renderCarouselSection()
3291
+ ) : (
2324
3292
  <>
2325
- {renderLabel('mediaLabel')}
2326
- <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
3293
+ {/* Show media only for rich_card */}
3294
+ {templateType === contentType.rich_card && (
3295
+ <>
3296
+ {renderLabel('mediaLabel')}
3297
+ <ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
2327
3298
  <CapRadioGroup
2328
- options={mediaRadioOptions || []}
2329
- value={templateMediaType}
2330
- onChange={onTemplateMediaTypeChange}
2331
- disabled={(isEditFlow || !isFullMode)}
2332
- className="rcs-radio"
2333
- />
2334
- </ConfigProvider>
3299
+ options={mediaRadioOptions || []}
3300
+ value={templateMediaType}
3301
+ onChange={onTemplateMediaTypeChange}
3302
+ disabled={(isEditFlow || !isFullMode)}
3303
+ className="rcs-radio"
3304
+ />
3305
+ </ConfigProvider>
2335
3306
  <div className="rcs-container-image">
2336
- {getMediaBasedComponent()}
2337
- </div>
3307
+ {getMediaBasedComponent()}
3308
+ </div>
3309
+ </>
3310
+ )}
3311
+ {renderTextComponent()}
3312
+ </>
3313
+ )}
3314
+ {(!isEditFlow || hasMeaningfulSmsFallbackShape(smsFallbackData)) && (
3315
+ <>
3316
+ <CapDivider className="rcs-fallback-section-divider" />
3317
+ {renderFallBackSmsComponent()}
2338
3318
  </>
2339
3319
  )}
2340
- {renderTextComponent()}
2341
- <CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
2342
- {renderFallBackSmsComponent()}
2343
3320
  <div className="rcs-scroll-div" />
2344
3321
  </CapColumn>
2345
3322
  <CapColumn span={8} offset={2}>
@@ -2350,7 +3327,8 @@ const splitTemplateVarString = (str) => {
2350
3327
 
2351
3328
 
2352
3329
  <div className="rcs-footer">
2353
- {!isEditFlow && (
3330
+ {/* 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). */}
3331
+ {!isEditFlow && isFullMode && (
2354
3332
  <>
2355
3333
  <div className="button-disabled-tooltip-wrapper">
2356
3334
  <CapButton
@@ -2358,7 +3336,9 @@ const splitTemplateVarString = (str) => {
2358
3336
  disabled={isDisableDone()}
2359
3337
  className="rcs-done-btn"
2360
3338
  >
2361
- <FormattedMessage {...messages.sendForApprovalButtonLabel} />
3339
+ <FormattedMessage
3340
+ {...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
3341
+ />
2362
3342
  </CapButton>
2363
3343
  </div>
2364
3344
  <CapTooltip
@@ -2371,7 +3351,6 @@ const splitTemplateVarString = (str) => {
2371
3351
  className="rcs-test-preview-btn"
2372
3352
  type="secondary"
2373
3353
  disabled={true}
2374
- style={{ marginLeft: "8px" }}
2375
3354
  >
2376
3355
  <FormattedMessage {...creativesMessages.testAndPreview} />
2377
3356
  </CapButton>
@@ -2392,7 +3371,7 @@ const splitTemplateVarString = (str) => {
2392
3371
  </div>
2393
3372
  </>
2394
3373
  )}
2395
- {isEditFlow && templateStatus === RCS_STATUSES.approved && (
3374
+ {isEditFlow && (isHostInfoBip || templateStatus === RCS_STATUSES.approved) && (
2396
3375
  <>
2397
3376
  <CapButton
2398
3377
  onClick={handleTestAndPreview}
@@ -2404,51 +3383,6 @@ const splitTemplateVarString = (str) => {
2404
3383
  </>
2405
3384
  )}
2406
3385
  </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
3386
  </>
2453
3387
  );
2454
3388
  };
@@ -2457,29 +3391,49 @@ const splitTemplateVarString = (str) => {
2457
3391
  <CapSpin spinning={loadingTags || spin}>
2458
3392
  {getMainContent()}
2459
3393
  </CapSpin>
3394
+
2460
3395
  <TestAndPreviewSlidebox
2461
3396
  show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
2462
3397
  onClose={handleCloseTestAndPreview}
2463
- formData={null} // RCS doesn't use formData structure like SMS
2464
- content={getTemplateContent()}
3398
+ formData={testPreviewFormData}
3399
+ content={testAndPreviewContent}
2465
3400
  currentChannel={RCS}
3401
+ orgUnitId={orgUnitId}
3402
+ rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
3403
+ smsFallbackContent={
3404
+ smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
3405
+ ? {
3406
+ templateContent:
3407
+ smsFallbackData.templateContent || smsFallbackData.content || '',
3408
+ templateName: smsFallbackData.templateName || '',
3409
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
3410
+ ? mergeRcsSmsFallbackVarMapLayers(
3411
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
3412
+ smsFallbackData,
3413
+ )
3414
+ : mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
3415
+ }
3416
+ : null
3417
+ }
3418
+ smsRegister={smsRegister}
2466
3419
  />
2467
3420
  </>
2468
3421
  );
2469
3422
  };
2470
3423
 
3424
+
2471
3425
  const mapStateToProps = createStructuredSelector({
2472
3426
  rcsData: makeSelectRcs(),
2473
3427
  accountData: makeSelectAccount(),
2474
3428
  metaEntities: makeSelectMetaEntities(),
2475
3429
  loadingTags: isLoadingMetaEntities(),
2476
3430
  injectedTags: setInjectedTags(),
3431
+ currentOrgDetails: selectCurrentOrgDetails(),
2477
3432
  });
2478
3433
 
2479
3434
  const mapDispatchToProps = (dispatch) => ({
2480
3435
  actions: bindActionCreators(RcsActions, dispatch),
2481
3436
  globalActions: bindActionCreators(globalActions, dispatch),
2482
- templatesActions: bindActionCreators(TemplatesActions, dispatch),
2483
3437
  });
2484
3438
 
2485
3439
  const withReducer = injectReducer({ key: 'rcs', reducer: v2RcsReducer });