@capillarytech/creatives-library 9.0.26 → 9.0.29-alpha.1

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