@capillarytech/creatives-library 8.0.316-alpha.2 → 8.0.316-alpha.4

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 (107) hide show
  1. package/constants/unified.js +14 -0
  2. package/package.json +1 -1
  3. package/utils/templateVarUtils.js +172 -0
  4. package/utils/tests/templateVarUtils.test.js +160 -0
  5. package/v2Components/CapTagList/index.js +10 -0
  6. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  7. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  8. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  9. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  13. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  15. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  16. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  17. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  18. package/v2Components/CommonTestAndPreview/index.js +693 -155
  19. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  20. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  21. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  22. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +172 -0
  23. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  24. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  25. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +245 -0
  26. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  27. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +100 -1
  28. package/v2Components/CommonTestAndPreview/tests/index.test.js +19 -1
  29. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  30. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  31. package/v2Components/FormBuilder/index.js +13 -13
  32. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  33. package/v2Components/SmsFallback/constants.js +73 -0
  34. package/v2Components/SmsFallback/index.js +956 -0
  35. package/v2Components/SmsFallback/index.scss +265 -0
  36. package/v2Components/SmsFallback/messages.js +78 -0
  37. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  38. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  39. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  40. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  41. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  42. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  43. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +327 -0
  44. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  45. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  46. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  47. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  48. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  49. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  50. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  51. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  52. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  53. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  54. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  55. package/v2Containers/CreativesContainer/constants.js +9 -0
  56. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  57. package/v2Containers/CreativesContainer/index.js +286 -93
  58. package/v2Containers/CreativesContainer/index.scss +51 -1
  59. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  60. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  61. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  62. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  63. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  64. package/v2Containers/InApp/index.js +4 -5
  65. package/v2Containers/Rcs/constants.js +32 -1
  66. package/v2Containers/Rcs/index.js +953 -882
  67. package/v2Containers/Rcs/index.scss +85 -6
  68. package/v2Containers/Rcs/messages.js +10 -1
  69. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  70. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  71. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  72. package/v2Containers/Rcs/tests/index.test.js +41 -38
  73. package/v2Containers/Rcs/tests/mockData.js +38 -0
  74. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  75. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  76. package/v2Containers/Rcs/utils.js +358 -10
  77. package/v2Containers/Sms/Create/index.js +81 -36
  78. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  79. package/v2Containers/SmsTrai/Create/index.js +9 -4
  80. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  81. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  82. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  83. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  84. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  85. package/v2Containers/SmsWrapper/index.js +37 -8
  86. package/v2Containers/TagList/index.js +6 -0
  87. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  88. package/v2Containers/Templates/_templates.scss +61 -2
  89. package/v2Containers/Templates/actions.js +11 -0
  90. package/v2Containers/Templates/constants.js +2 -0
  91. package/v2Containers/Templates/index.js +90 -40
  92. package/v2Containers/Templates/sagas.js +57 -12
  93. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  94. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  95. package/v2Containers/Templates/tests/sagas.test.js +110 -12
  96. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  97. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  98. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  99. package/v2Containers/TemplatesV2/index.js +86 -23
  100. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  101. package/v2Containers/Viber/index.js +4 -9
  102. package/v2Containers/WebPush/Create/components/MessageSection.js +54 -18
  103. package/v2Containers/WebPush/Create/components/MessageSection.test.js +28 -0
  104. package/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +7 -3
  105. package/v2Containers/Whatsapp/index.js +10 -32
  106. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  107. package/v2Containers/Whatsapp/tests/index.test.js +172 -0
@@ -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';
@@ -22,44 +21,34 @@ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
22
21
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
23
22
  import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
24
23
  import CapImage from '@capillarytech/cap-ui-library/CapImage';
25
- import CapCard from '@capillarytech/cap-ui-library/CapCard';
26
24
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
27
25
  import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
28
- import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
29
- import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
30
- import CapMenu from '@capillarytech/cap-ui-library/CapMenu';
31
26
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
32
- import CapTooltipWithInfo from '@capillarytech/cap-ui-library/CapTooltipWithInfo';
33
27
  import CapError from '@capillarytech/cap-ui-library/CapError';
34
- import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
35
28
  import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
36
- import CapLink from '@capillarytech/cap-ui-library/CapLink';
37
-
38
- import {
39
- CAP_G01,
40
- CAP_SPACE_04,
41
- CAP_SPACE_16,
42
- CAP_SPACE_24,
43
- CAP_SPACE_28,
44
- CAP_SPACE_32,
45
- CAP_WHITE,
46
- CAP_SECONDARY,
47
- } from '@capillarytech/cap-ui-library/styled/variables';
48
29
 
49
30
  import CapVideoUpload from '../../v2Components/CapVideoUpload';
50
31
  import * as globalActions from '../Cap/actions';
51
32
  import CapActionButton from '../../v2Components/CapActionButton';
52
33
  import { makeSelectRcs, makeSelectAccount } from './selectors';
53
- import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
54
34
  import {
55
35
  isLoadingMetaEntities,
56
36
  makeSelectMetaEntities,
57
37
  setInjectedTags,
58
- selectCurrentOrgDetails,
59
38
  } from '../Cap/selectors';
60
39
  import * as RcsActions from './actions';
40
+ import { isAiContentBotDisabled } from '../../utils/common';
61
41
  import * as TemplatesActions from '../Templates/actions';
62
42
  import './index.scss';
43
+ import {
44
+ normalizeLibraryLoadedTitleDesc,
45
+ mergeRcsSmsFallBackContentFromDetails,
46
+ mergeRcsSmsFallbackVarMapLayers,
47
+ pickFirstSmsFallbackTemplateString,
48
+ syncCardVarMappedSemanticsFromSlots,
49
+ hasMeaningfulSmsFallbackShape,
50
+ getLibrarySmsFallbackApiBaselineFromTemplateData,
51
+ } from './rcsLibraryHydrationUtils';
63
52
  import {
64
53
  RCS,
65
54
  SMS,
@@ -76,14 +65,11 @@ import {
76
65
  RCS_IMG_SIZE,
77
66
  RCS_DLT_MODE,
78
67
  CTA,
79
- AI_CONTENT_BOT_DISABLED,
80
68
  RCS_STATUSES,
81
69
  TITLE_TEXT,
82
70
  MESSAGE_TEXT,
83
71
  ALLOWED_EXTENSIONS_VIDEO_REGEX,
84
72
  RCS_VIDEO_SIZE,
85
- TEMPLATE_HEADER_MAX_LENGTH,
86
- TEMPLATE_MESSAGE_MAX_LENGTH,
87
73
  RCS_THUMBNAIL_MIN_SIZE,
88
74
  RCS_THUMBNAIL_MAX_SIZE,
89
75
  contentType,
@@ -96,35 +82,45 @@ import {
96
82
  MAX_BUTTONS,
97
83
  INITIAL_SUGGESTIONS_DATA_STOP,
98
84
  RCS_BUTTON_TYPES,
99
- titletype,
100
- descType,
101
85
  STANDALONE,
102
86
  VERTICAL,
103
87
  SMALL,
104
88
  MEDIUM,
105
89
  RICHCARD,
90
+ RCS_NUMERIC_VAR_NAME_REGEX,
91
+ RCS_TAG_AREA_FIELD_TITLE,
92
+ RCS_TAG_AREA_FIELD_DESC,
106
93
  } from './constants';
107
94
  import globalMessages from '../Cap/messages';
108
95
  import messages from './messages';
109
96
  import creativesMessages from '../CreativesContainer/messages';
110
97
  import withCreatives from '../../hoc/withCreatives';
111
98
  import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
112
- import { ANDROID } from '../../v2Components/CommonTestAndPreview/constants';
99
+ import VarSegmentMessageEditor from '../../v2Components/VarSegmentMessageEditor';
100
+ import { ANDROID, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
113
101
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
102
+ import { splitTemplateVarString } from '../../utils/templateVarUtils';
114
103
  import CapImageUpload from '../../v2Components/CapImageUpload';
115
- import addCreativesIcon from '../Assets/images/addCreativesIllustration.svg';
116
104
  import Templates from '../Templates';
117
105
  import SmsTraiEdit from '../SmsTrai/Edit';
106
+ import SmsFallback from '../../v2Components/SmsFallback';
107
+ import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
118
108
  import TagList from '../TagList';
119
109
  import { validateTags } from '../../utils/tagValidations';
120
- import { getCdnUrl } from '../../utils/cdnTransformation';
110
+ import { isTraiDLTEnable } from '../../utils/common';
121
111
  import { isTagIncluded } from '../../utils/commonUtils';
122
112
  import injectReducer from '../../utils/injectReducer';
123
113
  import v2RcsReducer from './reducer';
124
- import { getTemplateStatusType } from './utils';
125
-
114
+ import {
115
+ areAllRcsSmsFallbackVarSlotsFilled,
116
+ buildRcsNumericMustachePlaceholderRegex,
117
+ getTemplateStatusType,
118
+ normalizeCardVarMapped,
119
+ coalesceCardVarMappedToTemplate,
120
+ resolveCardVarMappedSlotValue,
121
+ sanitizeCardVarMappedValue,
122
+ } from './utils';
126
123
 
127
- const { Group: CapCheckboxGroup } = CapCheckbox;
128
124
  export const Rcs = (props) => {
129
125
  const {
130
126
  intl,
@@ -138,17 +134,15 @@ export const Rcs = (props) => {
138
134
  templatesActions,
139
135
  globalActions,
140
136
  location,
141
- handleClose,
142
137
  getDefaultTags,
143
138
  supportedTags,
144
139
  metaEntities,
145
140
  injectedTags,
146
141
  loadingTags,
147
142
  getFormData,
148
- isDltEnabled,
149
143
  smsRegister,
144
+ orgUnitId,
150
145
  selectedOfferDetails,
151
- currentOrgDetails,
152
146
  eventContextTags,
153
147
  accountData = {},
154
148
  // TestAndPreviewSlidebox props
@@ -158,8 +152,8 @@ export const Rcs = (props) => {
158
152
  } = props || {};
159
153
  const { formatMessage } = intl;
160
154
  const { TextArea } = CapInput;
161
- const { CapCustomCardList } = CapCustomCard;
162
155
  const [isEditFlow, setEditFlow] = useState(false);
156
+ const isEditLike = isEditFlow || !isFullMode;
163
157
  const [tags, updateTags] = useState([]);
164
158
  const [spin, setSpin] = useState(false);
165
159
  //template
@@ -168,112 +162,71 @@ export const Rcs = (props) => {
168
162
  const [templateMediaType, setTemplateMediaType] = useState(
169
163
  RCS_MEDIA_TYPES.NONE,
170
164
  );
171
- const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
172
165
  const [templateTitle, setTemplateTitle] = useState('');
173
166
  const [templateDesc, setTemplateDesc] = useState('');
174
167
  const [templateDescError, setTemplateDescError] = useState(false);
175
168
  const [templateStatus, setTemplateStatus] = useState('');
176
- const [templateDate, setTemplateDate] = useState('');
177
- //fallback
178
- const [fallbackMessage, setFallbackMessage] = useState('');
179
- const [fallbackMessageError, setFallbackMessageError] = useState(false);
180
169
  //fallback dlt
181
170
  const [showDltContainer, setShowDltContainer] = useState(false);
182
171
  const [dltMode, setDltMode] = useState('');
183
172
  const [dltEditData, setDltEditData] = useState({});
184
- const [showDltCard, setShowDltCard] = useState(false);
185
- const [fallbackPreviewmode, setFallbackPreviewmode] = useState(false);
186
- const [dltPreviewData, setDltPreviewData] = useState('');
173
+ /** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
174
+ const [smsFallbackData, setSmsFallbackData] = useState(undefined);
187
175
  const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
188
- const [buttonType, setButtonType] = useState(RCS_BUTTON_TYPES.NONE);
176
+ const buttonType = RCS_BUTTON_TYPES.NONE;
189
177
  const [suggestionError, setSuggestionError] = useState(true);
190
178
  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
179
  const [titleVarMappedData, setTitleVarMappedData] = useState({});
199
180
  const [descVarMappedData, setDescVarMappedData] = useState({});
200
181
  const [titleTextAreaId, setTitleTextAreaId] = useState();
201
182
  const [descTextAreaId, setDescTextAreaId] = useState();
202
183
  const [assetList, setAssetList] = useState({});
203
- const [assetListImage, setAssetListImage] = useState('');
204
184
  const [rcsImageSrc, updateRcsImageSrc] = useState('');
205
185
  const [rcsVideoSrc, setRcsVideoSrc] = useState({});
206
186
  const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
207
187
  const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
208
- const [imageError, setImageError] = useState(null);
209
188
  const [templateTitleError, setTemplateTitleError] = useState(false);
210
189
  const [cardVarMapped, setCardVarMapped] = useState({});
190
+ /** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
191
+ const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
192
+ const lastHydratedRcsCardVarSignatureRef = useRef(null);
193
+
194
+ /**
195
+ * Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
196
+ * without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
197
+ * fallback card / content stopped appearing until re-selected.
198
+ */
199
+ const rcsHydrationDetails = useMemo(
200
+ () => (isFullMode ? rcsData?.templateDetails : templateData),
201
+ [isFullMode, rcsData?.templateDetails, templateData],
202
+ );
211
203
 
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;
204
+ /** 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'). */
205
+ const lastTagSchemaQueryKeyRef = useRef(null);
206
+ /**
207
+ * Library: parent often passes a new `templateData` object reference every render. Re-applying the same
208
+ * SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
209
+ */
210
+ const lastSmsFallbackHydrationKeyRef = useRef(null);
211
+
212
+ const fetchTagSchemaIfNewQuery = useCallback(
213
+ (query) => {
214
+ const key = JSON.stringify(query);
215
+ if (lastTagSchemaQueryKeyRef.current === key) {
216
+ return;
243
217
  }
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
- };
218
+ lastTagSchemaQueryKeyRef.current = key;
219
+ globalActions.fetchSchemaForEntity(query);
220
+ },
221
+ [globalActions],
222
+ );
260
223
 
261
- return contentObj;
262
- }, [
263
- templateMediaType,
264
- templateTitle,
265
- templateDesc,
266
- rcsImageSrc,
267
- rcsVideoSrc,
268
- rcsThumbnailSrc,
269
- suggestions,
270
- selectedDimension,
271
- ]);
224
+ // TestAndPreviewSlidebox state
225
+ const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
272
226
 
273
227
  // Handle Test and Preview button click
274
228
  const handleTestAndPreview = useCallback(() => {
275
229
  setShowTestAndPreviewSlidebox(true);
276
- setIsTestAndPreviewMode(true);
277
230
  if (propsHandleTestAndPreview) {
278
231
  propsHandleTestAndPreview();
279
232
  }
@@ -282,23 +235,24 @@ export const Rcs = (props) => {
282
235
  // Handle close Test and Preview slidebox
283
236
  const handleCloseTestAndPreview = useCallback(() => {
284
237
  setShowTestAndPreviewSlidebox(false);
285
- setIsTestAndPreviewMode(false);
286
238
  if (propsHandleCloseTestAndPreview) {
287
239
  propsHandleCloseTestAndPreview();
288
240
  }
289
241
  }, [propsHandleCloseTestAndPreview]);
290
242
 
291
- // Helper to get RCS orientation from selectedDimension
292
- const getRcsOrientation = () => {
293
- const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
294
- if (isMediaTypeImage) {
295
- return RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
296
- }
297
- // For video
298
- return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
299
- };
243
+ /** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
244
+ const handleSmsFallbackEditorStateChange = useCallback((patch) => {
245
+ setSmsFallbackData((prev) => {
246
+ if (!patch || typeof patch !== 'object') return prev;
247
+ // Merge even when `prev` is null so tag/slot updates apply on top of `smsFromApiShape` in createPayload.
248
+ return { ...(prev || {}), ...patch };
249
+ });
250
+ }, []);
300
251
 
252
+ /** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
301
253
  const [accountId, setAccountId] = useState('');
254
+ /** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
255
+ const [wecrmAccountId, setWecrmAccountId] = useState('');
302
256
  const [accessToken, setAccessToken] = useState('');
303
257
  const [hostName, setHostName] = useState('');
304
258
  const [accountName, setAccountName] = useState('');
@@ -306,14 +260,23 @@ export const Rcs = (props) => {
306
260
  const accountObj = accountData.selectedRcsAccount || {};
307
261
  if (!isEmpty(accountObj)) {
308
262
  const {
263
+ id: wecrmId,
309
264
  sourceAccountIdentifier = '',
310
265
  configs = {},
311
266
  } = accountObj;
312
-
313
267
  setAccountId(sourceAccountIdentifier);
268
+ setWecrmAccountId(
269
+ wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
270
+ );
314
271
  setAccessToken(configs.accessToken || '');
315
272
  setHostName(accountObj.hostName || '');
316
273
  setAccountName(accountObj.name || '');
274
+ } else {
275
+ setAccountId('');
276
+ setWecrmAccountId('');
277
+ setAccessToken('');
278
+ setHostName('');
279
+ setAccountName('');
317
280
  }
318
281
  }, [accountData.selectedRcsAccount]);
319
282
 
@@ -331,10 +294,7 @@ export const Rcs = (props) => {
331
294
  label: formatMessage(messages.mediaVideo),
332
295
  },
333
296
  ];
334
- const { accessibleFeatures = [] } = currentOrgDetails || {};
335
- const isAiContentBotDisabled = accessibleFeatures?.includes(
336
- AI_CONTENT_BOT_DISABLED
337
- );
297
+ const aiContentBotDisabled = isAiContentBotDisabled();
338
298
 
339
299
  const updateButtonChange = (data, index) => {
340
300
  if (data && data.text) {
@@ -372,7 +332,9 @@ export const Rcs = (props) => {
372
332
  if (isFullMode) return;
373
333
  if (loadingTags || !tags || tags.length === 0) return;
374
334
  const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
375
- const resolved = resolveTemplateWithMap(templateStr); // placeholders -> mapped value (or '')
335
+ const slotOffset =
336
+ type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
337
+ const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
376
338
  if (!resolved) {
377
339
  if (type === TITLE_TEXT) setTemplateTitleError(false);
378
340
  if (type === MESSAGE_TEXT) setTemplateDescError(false);
@@ -417,13 +379,41 @@ export const Rcs = (props) => {
417
379
 
418
380
  const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
419
381
 
420
- const resolveTemplateWithMap = (str = '') => {
382
+ const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
383
+
384
+ /** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
385
+ const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
386
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
387
+ const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
388
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
389
+ let varOrdinal = 0;
390
+ for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
391
+ const segmentToken = templateSegments[segmentIndexInField];
392
+ if (rcsVarTestRegex.test(segmentToken)) {
393
+ if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
394
+ return offset + varOrdinal;
395
+ }
396
+ varOrdinal += 1;
397
+ }
398
+ }
399
+ return null;
400
+ };
401
+
402
+ const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
421
403
  if (!str) return '';
422
- const arr = splitTemplateVarString(str);
404
+ const arr = splitTemplateVarStringRcs(str);
405
+ let varOrdinal = 0;
423
406
  return arr.map((elem) => {
424
407
  if (rcsVarTestRegex.test(elem)) {
425
408
  const key = getVarNameFromToken(elem);
426
- const v = cardVarMapped?.[key];
409
+ const globalSlot = slotOffset + varOrdinal;
410
+ varOrdinal += 1;
411
+ const v = resolveCardVarMappedSlotValue(
412
+ cardVarMapped,
413
+ key,
414
+ globalSlot,
415
+ isEditLike,
416
+ );
427
417
  if (isNil(v) || String(v)?.trim?.() === '') return elem;
428
418
  return String(v);
429
419
  }
@@ -431,107 +421,154 @@ export const Rcs = (props) => {
431
421
  }).join('');
432
422
  };
433
423
 
424
+ /**
425
+ * Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
426
+ * (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
427
+ * TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
428
+ */
429
+ const getTemplateContent = useCallback(() => {
430
+ const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
431
+ const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
432
+ const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
434
433
 
435
- useEffect(() => {
436
- if (isFullMode || isEditFlow) return;
437
- const tokens = [
438
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
439
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
440
- ];
441
- if (!tokens.length) return;
442
- setCardVarMapped((prev) => {
443
- const next = { ...(prev || {}) };
444
- let changed = false;
445
- tokens.forEach((t) => {
446
- const name = getVarNameFromToken(t);
447
- if (name && !Object.prototype.hasOwnProperty.call(next, name)) {
448
- next[name] = '';
449
- changed = true;
450
- }
451
- });
452
- return changed ? next : prev;
453
- });
454
- }, [isFullMode, templateTitle, templateDesc]);
434
+ const isSlotMappingMode = isEditFlow || !isFullMode;
435
+ const titleVarCountForResolve = isMediaTypeText
436
+ ? 0
437
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
438
+ const resolvedTitle = isMediaTypeText
439
+ ? ''
440
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
441
+ const resolvedDesc = isSlotMappingMode
442
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
443
+ : templateDesc;
444
+
445
+ const mediaPreview = {};
446
+ if (isMediaTypeImage && rcsImageSrc) {
447
+ mediaPreview.rcsImageSrc = rcsImageSrc;
448
+ }
449
+ if (isMediaTypeVideo && !isMediaTypeText) {
450
+ if (rcsThumbnailSrc) {
451
+ mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
452
+ } else if (rcsVideoSrc?.videoSrc) {
453
+ mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
454
+ }
455
+ if (rcsThumbnailSrc) {
456
+ mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
457
+ }
458
+ }
459
+
460
+ const contentObj = {
461
+ templateHeader: resolvedTitle,
462
+ templateMessage: resolvedDesc,
463
+ ...mediaPreview,
464
+ ...(suggestions.length > 0 && {
465
+ suggestions: suggestions,
466
+ }),
467
+ };
468
+
469
+ return contentObj;
470
+ }, [
471
+ templateMediaType,
472
+ templateTitle,
473
+ templateDesc,
474
+ rcsImageSrc,
475
+ rcsVideoSrc,
476
+ rcsThumbnailSrc,
477
+ suggestions,
478
+ selectedDimension,
479
+ isFullMode,
480
+ isEditFlow,
481
+ cardVarMapped,
482
+ ]);
455
483
 
484
+ const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
456
485
 
457
- const RcsLabel = styled.div`
458
- display: flex;
459
- margin-top: 20px;
460
- `;
461
486
  const paramObj = params || {};
462
487
  useEffect(() => {
463
- const { id } = paramObj;
464
- if (id && isFullMode) {
465
- setSpin(true);
466
- actions.getTemplateDetails(id, setSpin);
467
- }
468
- return () => {
469
- actions.clearEditResponse();
470
- };
471
- }, [paramObj.id]);
488
+ const { id } = paramObj;
489
+ if (id && isFullMode) {
490
+ setSpin(true);
491
+ actions.getTemplateDetails(id, setSpin);
492
+ }
493
+ return () => {
494
+ actions.clearEditResponse();
495
+ };
496
+ }, [paramObj.id, isFullMode]);
472
497
 
473
498
  useEffect(() => {
474
- if (!(isEditFlow || !isFullMode)) return;
475
-
476
- const initField = (targetString, currentVarMap, setVarMap, setUpdated) => {
477
- const arr = splitTemplateVarString(targetString);
478
- if (!arr?.length) {
479
- setVarMap({});
480
- setUpdated([]);
481
- return;
499
+ if (!(isEditFlow || !isFullMode)) return;
500
+
501
+ const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
502
+
503
+ const initField = (targetString, setVarMap, slotOffset) => {
504
+ const arr = splitTemplateVarStringRcs(targetString);
505
+ if (!arr?.length) {
506
+ setVarMap({});
507
+ return;
508
+ }
509
+ const nextVarMap = {};
510
+ let varOrdinal = 0;
511
+ arr.forEach((elem, idx) => {
512
+ // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
513
+ if (rcsVarTestRegex.test(elem)) {
514
+ const id = `${elem}_${idx}`;
515
+ const varName = getVarNameFromToken(elem);
516
+ const globalSlot = slotOffset + varOrdinal;
517
+ varOrdinal += 1;
518
+ const mappedValue = resolveCardVarMappedSlotValue(
519
+ cardVarMapped,
520
+ varName,
521
+ globalSlot,
522
+ isEditLike,
523
+ );
524
+ nextVarMap[id] = mappedValue;
482
525
  }
483
- const nextVarMap = {};
484
- const nextUpdated = [...arr];
485
- arr.forEach((elem, idx) => {
486
- // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
487
- if (rcsVarTestRegex.test(elem)) {
488
- const id = `${elem}_${idx}`;
489
- const varName = getVarNameFromToken(elem);
490
- const mappedValue = (cardVarMapped?.[varName] ?? '').toString();
491
- nextVarMap[id] = mappedValue;
492
- if (mappedValue !== '') {
493
- nextUpdated[idx] = mappedValue;
494
- } else {
495
- nextUpdated[idx] = elem;
496
- }
497
- }
498
- });
499
- setVarMap(nextVarMap);
500
- setUpdated(nextUpdated);
501
- };
526
+ });
527
+ setVarMap(nextVarMap);
528
+ };
502
529
 
503
- initField(templateTitle, titleVarMappedData, setTitleVarMappedData, setUpdatedTitleData);
504
- initField(templateDesc, descVarMappedData, setDescVarMappedData, setUpdatedDescData);
505
- }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
506
-
507
- useEffect(() => {
508
- if(!isEditFlow && isFullMode){
530
+ initField(templateTitle, setTitleVarMappedData, 0);
531
+ initField(templateDesc, setDescVarMappedData, titleTokenCount);
532
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
533
+
534
+ useEffect(() => {
535
+ if (!isEditFlow && isFullMode) {
509
536
  setRcsVideoSrc({});
510
537
  updateRcsImageSrc('');
511
538
  setUpdateRcsImageSrc('');
512
539
  updateRcsThumbnailSrc('');
513
540
  setAssetList({});
514
- }
515
- }, [templateMediaType]);
541
+ }
542
+ }, [templateMediaType]);
516
543
 
517
- const templateStatusHelper = (details) => {
518
- const status = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
519
- switch (status) {
520
- case RCS_STATUSES.approved:
544
+ /** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
545
+ const templateStatusHelper = (cardContentFirst) => {
546
+ const raw =
547
+ cardContentFirst?.Status
548
+ ?? cardContentFirst?.status
549
+ ?? cardContentFirst?.approvalStatus
550
+ ?? '';
551
+ const status = typeof raw === 'string' ? raw.trim() : String(raw);
552
+ const n = status.toLowerCase();
553
+ switch (n) {
554
+ case 'approved':
521
555
  setTemplateStatus(RCS_STATUSES.approved);
522
556
  break;
523
- case RCS_STATUSES.pending:
557
+ case 'pending':
524
558
  setTemplateStatus(RCS_STATUSES.pending);
525
559
  break;
526
- case RCS_STATUSES.awaitingApproval:
560
+ case 'awaitingapproval':
527
561
  setTemplateStatus(RCS_STATUSES.awaitingApproval);
528
562
  break;
529
- case RCS_STATUSES.unavailable:
563
+ case 'unavailable':
530
564
  setTemplateStatus(RCS_STATUSES.unavailable);
531
565
  break;
532
- case RCS_STATUSES.rejected:
566
+ case 'rejected':
533
567
  setTemplateStatus(RCS_STATUSES.rejected);
534
568
  break;
569
+ case 'created':
570
+ setTemplateStatus(RCS_STATUSES.created);
571
+ break;
535
572
  default:
536
573
  setTemplateStatus(status);
537
574
  break;
@@ -542,7 +579,6 @@ export const Rcs = (props) => {
542
579
  if (mediaType) {
543
580
  setTemplateMediaType(mediaType);
544
581
  }
545
- const tempOrientation = cardSettings.cardOrientation;
546
582
  const tempAlignment = cardSettings.mediaAlignment;
547
583
  const tempHeight = mediaData.height;
548
584
 
@@ -585,44 +621,197 @@ export const Rcs = (props) => {
585
621
  };
586
622
 
587
623
  useEffect(() => {
588
- const details = isFullMode ? rcsData?.templateDetails : templateData;
624
+ const details = rcsHydrationDetails;
589
625
  if (details && Object.keys(details).length > 0) {
590
- if (!isFullMode) {
591
- const tempCardVarMapped = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
592
- setCardVarMapped(tempCardVarMapped);
626
+ // Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
627
+ const cardFromVersions = get(
628
+ details,
629
+ 'versions.base.content.RCS.rcsContent.cardContent[0]',
630
+ );
631
+ const cardFromTop = get(details, 'rcsContent.cardContent[0]');
632
+ const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
633
+ const cardVarMappedFromCardContent =
634
+ card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
635
+ ? card0.cardVarMapped
636
+ : {};
637
+ const cardVarMappedFromRootMirror =
638
+ details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
639
+ ? details.rcsCardVarMapped
640
+ : {};
641
+ // Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
642
+ // nested versions.cardContent[0].cardVarMapped is dropped on reload.
643
+ const mergedCardVarMappedFromPayload = {
644
+ ...cardVarMappedFromRootMirror,
645
+ ...cardVarMappedFromCardContent,
646
+ };
647
+ const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
648
+ const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
649
+ const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
650
+ Object.keys(mergedCardVarMappedFromPayload)
651
+ .sort()
652
+ .reduce((accumulator, mapKey) => {
653
+ accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
654
+ return accumulator;
655
+ }, {}),
656
+ )}`;
657
+ if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
658
+ lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
659
+ setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
593
660
  }
594
- const mediaType = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
661
+ const tokenListForMap = [
662
+ ...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
663
+ ...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
664
+ ];
665
+ const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
666
+ // Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
667
+ // getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
668
+ const cardVarMappedBeforeCoalesce = isFullMode
669
+ ? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
670
+ : { ...mergedCardVarMappedFromPayload };
671
+ const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
672
+ cardVarMappedBeforeCoalesce,
673
+ loadedTitleForMap,
674
+ loadedDescForMap,
675
+ rcsVarRegex,
676
+ );
677
+ const cardVarMappedAfterNumericSlotSync = !isFullMode
678
+ ? syncCardVarMappedSemanticsFromSlots(
679
+ cardVarMappedAfterCoalesce,
680
+ loadedTitleForMap,
681
+ loadedDescForMap,
682
+ rcsVarRegex,
683
+ )
684
+ : cardVarMappedAfterCoalesce;
685
+ const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
686
+ // Pre-populate variable/tag mappings while opening an existing template in edit flows
687
+ setCardVarMapped((previousVarMapState) => {
688
+ const previousVarMap = previousVarMapState ?? {};
689
+ if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
690
+ const previousVarMapKeys = Object.keys(previousVarMap);
691
+ const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
692
+ if (previousVarMapKeys.length === nextVarMapKeys.length) {
693
+ const allSlotValuesMatchPrevious = previousVarMapKeys.every(
694
+ (key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
695
+ );
696
+ if (allSlotValuesMatchPrevious) return previousVarMapState;
697
+ }
698
+ return hydratedCardVarMappedResult;
699
+ });
700
+ const mediaType =
701
+ card0.mediaType
702
+ || get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
595
703
  if (mediaType === RCS_MEDIA_TYPES.NONE) {
596
704
  setTemplateType(contentType.text_message);
597
705
  } else {
598
706
  setTemplateType(contentType.rich_card);
599
707
  }
600
708
  setEditFlow(true);
601
- setTemplateName(details.name || '');
602
- const loadedTitle = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].title', '');
603
- const loadedDesc = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].description', '');
604
- const loadedMap = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped', {});
605
- const normalizedTitle = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedTitle, loadedMap) : loadedTitle;
606
- const normalizedDesc = (!isFullMode && !isEmpty(loadedMap)) ? getUnmappedDesc(loadedDesc, loadedMap) : loadedDesc;
709
+ setTemplateName(details?.name || details?.creativeName || '');
710
+ const loadedTitle = loadedTitleForMap;
711
+ const loadedDesc = loadedDescForMap;
712
+ const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
713
+ loadedTitle,
714
+ loadedDesc,
715
+ isFullMode,
716
+ cardVarMappedAfterHydration: hydratedCardVarMappedResult,
717
+ rcsVarRegex,
718
+ });
607
719
  setTemplateTitle(normalizedTitle);
608
720
  setTemplateDesc(normalizedDesc);
609
- setSuggestions(get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []));
610
- templateStatusHelper(details);
611
- const mediaData = get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
612
- const cardSettings = get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '');
721
+ setSuggestions(
722
+ Array.isArray(card0.suggestions)
723
+ ? card0.suggestions
724
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
725
+ );
726
+ const cardForStatus = {
727
+ ...card0,
728
+ Status:
729
+ card0.Status
730
+ ?? card0.status
731
+ ?? card0.approvalStatus
732
+ ?? get(details, 'templateStatus')
733
+ ?? get(details, 'approvalStatus')
734
+ ?? get(details, 'creativeStatus')
735
+ ?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
736
+ ?? '',
737
+ };
738
+ templateStatusHelper(cardForStatus);
739
+ const mediaData =
740
+ card0.media != null && card0.media !== ''
741
+ ? card0.media
742
+ : get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
743
+ const cardSettings =
744
+ get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
745
+ || get(details, 'rcsContent.cardSettings', '');
613
746
  setMediaData(mediaData, mediaType, cardSettings);
747
+
748
+ const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
749
+ const base = get(smsFallbackContent, 'versions.base', {});
750
+ const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
751
+ const smsEditor = base['sms-editor'];
752
+ const fromNested = Array.isArray(updatedEditor)
753
+ ? updatedEditor.join('')
754
+ : (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
755
+ const fallbackMessage = smsFallbackContent.smsContent
756
+ || smsFallbackContent.smsTemplateContent
757
+ || smsFallbackContent.message
758
+ || fromNested
759
+ || '';
760
+ const varMappedFromPayload = smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
761
+ const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
762
+ const hasFallbackPayload =
763
+ smsFallbackContent
764
+ && Object.keys(smsFallbackContent).length > 0
765
+ && (
766
+ !!smsFallbackContent.smsTemplateName
767
+ || !!fallbackMessage
768
+ || hasVarMapped
769
+ );
770
+ if (hasFallbackPayload) {
771
+ if (!fallbackMessage && !hasVarMapped && process.env.NODE_ENV !== 'production') {
772
+ console.warn('[RCS SMS Fallback] No message text found in API response. Inspect shape:', smsFallbackContent);
773
+ }
774
+ const unicodeFromApi =
775
+ typeof smsFallbackContent.unicodeValidity === 'boolean'
776
+ ? smsFallbackContent.unicodeValidity
777
+ : (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
778
+ const nextSmsState = {
779
+ templateName: smsFallbackContent.smsTemplateName || '',
780
+ content: fallbackMessage,
781
+ templateContent: fallbackMessage,
782
+ unicodeValidity: unicodeFromApi,
783
+ ...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
784
+ };
785
+ const hydrationKey = JSON.stringify({
786
+ creativeKey: details._id || details.name || details.creativeName || '',
787
+ templateName: nextSmsState.templateName,
788
+ content: nextSmsState.content,
789
+ unicodeValidity: nextSmsState.unicodeValidity,
790
+ varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
791
+ });
792
+ if (
793
+ isFullMode
794
+ || lastSmsFallbackHydrationKeyRef.current !== hydrationKey
795
+ ) {
796
+ lastSmsFallbackHydrationKeyRef.current = hydrationKey;
797
+ setSmsFallbackData(nextSmsState);
798
+ }
799
+ } else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
800
+ lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
801
+ setSmsFallbackData(null);
802
+ }
614
803
  }
615
- }, [rcsData, templateData, isFullMode, isEditFlow]);
616
-
804
+ }, [rcsHydrationDetails, isFullMode]);
617
805
 
618
806
  useEffect(() => {
619
807
  if (templateType === contentType.text_message) {
620
808
  setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
621
- setTemplateTitle('');
622
- setTemplateTitleError('');
809
+ // Full-mode create only: switching to plain text clears draft title/media. Never clear when
810
+ // hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
623
811
  if (!isEditFlow && isFullMode) {
812
+ setTemplateTitle('');
813
+ setTemplateTitleError('');
624
814
  setUpdateRcsImageSrc('');
625
- setUpdateRcsVideoSrc({});
626
815
  setRcsVideoSrc({});
627
816
  setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
628
817
  }
@@ -649,7 +838,8 @@ export const Rcs = (props) => {
649
838
  if (!showDltContainer) {
650
839
  const { type, module } = location.query || {};
651
840
  const isEmbedded = type === EMBEDDED;
652
- const context = isEmbedded ? module : DEFAULT;
841
+ // Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
842
+ const context = isEmbedded ? module : 'outbound';
653
843
  const embedded = isEmbedded ? type : FULL;
654
844
  const query = {
655
845
  layout: SMS,
@@ -660,9 +850,9 @@ export const Rcs = (props) => {
660
850
  if (getDefaultTags) {
661
851
  query.context = getDefaultTags;
662
852
  }
663
- globalActions.fetchSchemaForEntity(query);
853
+ fetchTagSchemaIfNewQuery(query);
664
854
  }
665
- }, [showDltContainer]);
855
+ }, [showDltContainer, fetchTagSchemaIfNewQuery]);
666
856
 
667
857
  useEffect(() => {
668
858
  let tag = get(metaEntities, `tags.standard`, []);
@@ -685,39 +875,72 @@ export const Rcs = (props) => {
685
875
  context,
686
876
  embedded,
687
877
  };
688
- globalActions.fetchSchemaForEntity(query);
878
+ if (getDefaultTags) {
879
+ query.context = getDefaultTags;
880
+ }
881
+ fetchTagSchemaIfNewQuery(query);
882
+ };
883
+
884
+ const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
885
+ if (!templateStr || !numericVarName || !tagName) return templateStr;
886
+ const re = buildRcsNumericMustachePlaceholderRegex(numericVarName);
887
+ return templateStr.replace(re, `{{${tagName}}}`);
689
888
  };
690
889
 
691
- const onTagSelect = (data, areaId) => {
890
+ const onTagSelect = (data, areaId, field) => {
692
891
  if (!areaId) return;
693
892
  const sep = areaId.lastIndexOf('_');
694
893
  if (sep === -1) return;
695
- const numId = Number(areaId.slice(sep + 1));
696
- if (isNaN(numId)) return;
894
+ const slotSuffix = areaId.slice(sep + 1);
895
+ if (slotSuffix === '' || isNaN(Number(slotSuffix))) return;
697
896
  const token = areaId.slice(0, sep);
698
897
  const variableName = getVarNameFromToken(token);
699
898
  if (!variableName) return;
899
+ const isNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(variableName));
900
+ const fieldStr = field === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
901
+ const fieldType = field === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
902
+ const globalSlotForArea = getGlobalSlotIndexForRcsFieldId(areaId, fieldStr, fieldType);
903
+
700
904
  setCardVarMapped((prev) => {
701
905
  const base = (prev?.[variableName] ?? '').toString();
702
906
  const nextVal = `${base}{{${data}}}`;
703
- return {
704
- ...(prev || {}),
705
- [variableName]: nextVal,
706
- };
907
+ const next = { ...(prev || {}) };
908
+ if (isNumericSlot) {
909
+ delete next[variableName];
910
+ next[data] = nextVal;
911
+ } else {
912
+ next[variableName] = nextVal;
913
+ // resolveCardVarMappedSlotValue prefers numeric slot keys ("1","2",…) over semantic names;
914
+ // hydration may set "1": ''. Use global slot index — suffix is segment index (includes static text), not var ordinal.
915
+ if (globalSlotForArea !== null && globalSlotForArea !== undefined) {
916
+ next[String(globalSlotForArea + 1)] = nextVal;
917
+ }
918
+ }
919
+ return next;
707
920
  });
708
- };
709
921
 
710
- const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
711
-
712
- const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
713
-
714
- const onTagSelectFallback = (data) => {
715
- const tempMsg = `${fallbackMessage}{{${data}}}`;
716
- const error = fallbackMessageErrorHandler(tempMsg);
717
- setFallbackMessage(tempMsg);
718
- setFallbackMessageError(error);
922
+ if (isNumericSlot && (field === RCS_TAG_AREA_FIELD_TITLE || field === RCS_TAG_AREA_FIELD_DESC)) {
923
+ if (field === RCS_TAG_AREA_FIELD_TITLE) {
924
+ setTemplateTitle((prev) => {
925
+ const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
926
+ if (nextStr === prev) return prev;
927
+ setTemplateTitleError(variableErrorHandling(nextStr));
928
+ return nextStr;
929
+ });
930
+ } else {
931
+ setTemplateDesc((prev) => {
932
+ const nextStr = replaceNumericPlaceholderWithTagInTemplate(prev || '', variableName, data);
933
+ if (nextStr === prev) return prev;
934
+ setTemplateDescError(variableErrorHandling(nextStr));
935
+ return nextStr;
936
+ });
937
+ }
938
+ }
719
939
  };
720
940
 
941
+ const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
942
+
943
+ const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
721
944
 
722
945
  //removing optout tag for rcs
723
946
  const getRcsTags = () => {
@@ -730,15 +953,14 @@ export const Rcs = (props) => {
730
953
  };
731
954
  // tag Code end
732
955
 
733
- const renderLabel = (value, showLabel, desc) => {
734
- const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
956
+ const renderLabel = (value, desc) => {
735
957
  return (
736
958
  <>
737
- <RcsLabel>
959
+ <div className="rcs-form-section-heading">
738
960
  <CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
739
- </RcsLabel>
961
+ </div>
740
962
  {desc && (
741
- <CapLabel type="label3" style={{ marginBottom: '17px' }}>
963
+ <CapLabel type="label3" className="rcs-form-field-caption">
742
964
  {formatMessage(messages[desc])}
743
965
  </CapLabel>
744
966
  )}
@@ -824,47 +1046,6 @@ export const Rcs = (props) => {
824
1046
  setTemplateDescError(error);
825
1047
  };
826
1048
 
827
-
828
- const templateDescErrorHandler = (value) => {
829
- let errorMessage = false;
830
- const { isBraceError } = validateTags({
831
- content: value,
832
- tagsParam: tags,
833
- location,
834
- tagModule: getDefaultTags,
835
- isFullMode,
836
- }) || {};
837
-
838
- const maxLength = templateType === contentType.text_message
839
- ? RCS_TEXT_MESSAGE_MAX_LENGTH
840
- : RCS_RICH_CARD_MAX_LENGTH;
841
-
842
- if (value === '' && isMediaTypeText) {
843
- errorMessage = formatMessage(messages.emptyTemplateDescErrorMessage);
844
- } else if (value?.length > maxLength) {
845
- errorMessage = formatMessage(messages.templateMessageLengthError);
846
- }
847
-
848
- if (isBraceError) {
849
- errorMessage = formatMessage(globalMessages.unbalanacedCurlyBraces);
850
- }
851
- return errorMessage;
852
- };
853
-
854
-
855
- const onFallbackMessageChange = ({ target: { value } }) => {
856
- const error = fallbackMessageErrorHandler(value);
857
- setFallbackMessage(value);
858
- setFallbackMessageError(error);
859
- };
860
-
861
- const fallbackMessageErrorHandler = (value) => {
862
- if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
863
- return formatMessage(messages.fallbackMsgLenError);
864
- }
865
- return false;
866
- };
867
-
868
1049
  // Check for forbidden characters: square brackets [] and single curly braces {}
869
1050
  const forbiddenCharactersValidation = (value) => {
870
1051
  if (!value) return false;
@@ -915,39 +1096,12 @@ export const Rcs = (props) => {
915
1096
  }
916
1097
  return false;
917
1098
  };
918
-
919
- const templateHeaderErrorHandler = (value) => {
920
- let errorMessage = false;
921
- if (value?.length > TEMPLATE_HEADER_MAX_LENGTH) {
922
- errorMessage = formatMessage(messages.templateHeaderLengthError);
923
- } else {
924
- errorMessage = variableErrorHandling(value);
925
- }
926
- return errorMessage;
927
- };
928
-
929
-
930
- const templateMessageErrorHandler = (value) => {
931
- let errorMessage = false;
932
- if (value === '') {
933
- errorMessage = formatMessage(messages.emptyTemplateMessageErrorMessage);
934
- } else if (
935
- value?.length
936
- > TEMPLATE_MESSAGE_MAX_LENGTH
937
- ) {
938
- errorMessage = formatMessage(messages.templateMessageLengthError);
939
- } else {
940
- errorMessage = variableErrorHandling(value);
941
- }
942
- return errorMessage;
943
- };
944
-
945
1099
 
946
1100
  const onMessageAddVar = () => {
947
- onAddVar(MESSAGE_TEXT, templateDesc, rcsVarRegex);
1101
+ onAddVar(templateDesc);
948
1102
  };
949
1103
 
950
- const onAddVar = (type, messageContent, regex) => {
1104
+ const onAddVar = (messageContent) => {
951
1105
  // Always append the next variable at the end, like WhatsApp
952
1106
  const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
953
1107
  const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
@@ -987,66 +1141,6 @@ const onTitleAddVar = () => {
987
1141
  setTemplateTitleError(error);
988
1142
  };
989
1143
 
990
-
991
- const splitTemplateVarString = (str) => {
992
- if (!str) return [];
993
- const validVarArr = str.match(rcsVarRegex) || [];
994
- const templateVarArray = [];
995
- let content = str;
996
- while (content?.length !== 0) {
997
- const index = content.indexOf(validVarArr?.[0]);
998
- if (index !== -1) {
999
- templateVarArray.push(content.substring(0, index));
1000
- templateVarArray.push(validVarArr?.[0]);
1001
- content = content.substring(index + validVarArr?.[0]?.length, content?.length);
1002
- validVarArr?.shift();
1003
- } else {
1004
- templateVarArray.push(content);
1005
- break;
1006
- }
1007
- }
1008
- return templateVarArray.filter(Boolean);
1009
- };
1010
-
1011
- const textAreaValue = (idValue, type) => {
1012
- if (idValue >= 0) {
1013
- const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
1014
- const templateArr = splitTemplateVarString(templateStr);
1015
- const token = templateArr?.[idValue] || "";
1016
- if (token && rcsVarTestRegex.test(token)) {
1017
- const varName = getVarNameFromToken(token);
1018
- return (cardVarMapped?.[varName] ?? '').toString();
1019
- }
1020
- return "";
1021
- }
1022
- return "";
1023
- };
1024
-
1025
- const textAreaValueChange = (e, type) => {
1026
- const value = e?.target?.value ?? '';
1027
- const id = e?.target?.id || e?.currentTarget?.id || '';
1028
- if (!id) return;
1029
- const sep = id.lastIndexOf('_');
1030
- if (sep === -1) return;
1031
- const isInvalidValue = value?.trim() === "";
1032
- const token = id.slice(0, sep);
1033
- const variableName = getVarNameFromToken(token);
1034
-
1035
- if (variableName) {
1036
- setCardVarMapped((prev) => ({
1037
- ...prev,
1038
- [variableName]: isInvalidValue ? "" : value,
1039
- }));
1040
- }
1041
- };
1042
-
1043
- const setTextAreaId = (e, type) => {
1044
- const id = e?.target?.id || e?.currentTarget?.id || '';
1045
- if (!id) return;
1046
- if (type === TITLE_TEXT) setTitleTextAreaId(id);
1047
- else setDescTextAreaId(id);
1048
- };
1049
-
1050
1144
  const renderButtonComponent = () => {
1051
1145
  return (
1052
1146
  <>
@@ -1077,39 +1171,56 @@ const splitTemplateVarString = (str) => {
1077
1171
  );
1078
1172
  };
1079
1173
 
1080
- const renderedRCSEditMessage = (descArray, type) => {
1081
- const renderArray = [];
1082
- if (descArray?.length) {
1083
- descArray.forEach((elem, index) => {
1084
- if (rcsVarTestRegex.test(elem)) {
1085
- // Variable input
1086
- renderArray.push(
1087
- <TextArea
1088
- id={`${elem}_${index}`}
1089
- key={`${elem}_${index}`}
1090
- placeholder={`enter the value for ${elem}`}
1091
- autosize={{ minRows: 1, maxRows: 3 }}
1092
- onChange={e => textAreaValueChange(e, type)}
1093
- value={textAreaValue(index, type)}
1094
- onFocus={(e) => setTextAreaId(e, type)}
1095
- />
1096
- );
1097
- } else if (elem) {
1098
- // Static text
1099
- renderArray.push(
1100
- <TextArea
1101
- key={`static_${index}`}
1102
- value={elem}
1103
- autosize={{ minRows: 1, maxRows: 3 }}
1104
- disabled
1105
- className="rcs-edit-template-message-static-textarea"
1106
- style={{ background: '#fafafa', color: '#888' }}
1107
- />
1108
- );
1109
- }
1110
- });
1111
- }
1112
- return renderArray;
1174
+ const getRcsValueMap = (fieldTemplateString, fieldType) => {
1175
+ if (!fieldTemplateString) return {};
1176
+ const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
1177
+ const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
1178
+ const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
1179
+ const segmentIdToResolvedValue = {};
1180
+ let varOrdinal = 0;
1181
+ templateSegments.forEach((segmentToken, segmentIndexInField) => {
1182
+ if (rcsVarTestRegex.test(segmentToken)) {
1183
+ const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
1184
+ const varName = getVarNameFromToken(segmentToken);
1185
+ const globalSlot = slotOffset + varOrdinal;
1186
+ varOrdinal += 1;
1187
+ segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
1188
+ cardVarMapped,
1189
+ varName,
1190
+ globalSlot,
1191
+ isEditLike,
1192
+ );
1193
+ }
1194
+ });
1195
+ return segmentIdToResolvedValue;
1196
+ };
1197
+
1198
+ const titleVarSegmentValueMapById = useMemo(
1199
+ () => getRcsValueMap(templateTitle, TITLE_TEXT),
1200
+ [templateTitle, cardVarMapped, isEditFlow, isFullMode],
1201
+ );
1202
+ const descriptionVarSegmentValueMapById = useMemo(
1203
+ () => getRcsValueMap(templateDesc, MESSAGE_TEXT),
1204
+ [templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode],
1205
+ );
1206
+
1207
+ const handleRcsVarChange = (id, value, type) => {
1208
+ const sep = id.lastIndexOf('_');
1209
+ if (sep === -1) return;
1210
+ const token = id.slice(0, sep);
1211
+ const variableName = getVarNameFromToken(token);
1212
+ if (variableName === undefined || variableName === null || variableName === '') return;
1213
+ const isInvalidValue = value?.trim() === '';
1214
+ const coercedSlotValue = isInvalidValue ? '' : value;
1215
+ const fieldStr = type === TITLE_TEXT ? templateTitle : templateDesc;
1216
+ const globalSlot = getGlobalSlotIndexForRcsFieldId(id, fieldStr, type);
1217
+ setCardVarMapped((previousVarMap) => {
1218
+ const nextVarMap = { ...previousVarMap, [variableName]: coercedSlotValue };
1219
+ if (globalSlot !== null && globalSlot !== undefined) {
1220
+ nextVarMap[String(globalSlot + 1)] = coercedSlotValue;
1221
+ }
1222
+ return nextVarMap;
1223
+ });
1113
1224
  };
1114
1225
 
1115
1226
  const renderTextComponent = () => {
@@ -1153,10 +1264,19 @@ const splitTemplateVarString = (str) => {
1153
1264
  </>
1154
1265
  }
1155
1266
  />
1156
- <div className="rcs_text_area_wrapper">
1157
1267
  {(isEditFlow || !isFullMode) ? (
1158
- renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
1268
+ <VarSegmentMessageEditor
1269
+ key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
1270
+ templateString={templateTitle}
1271
+ valueMap={titleVarSegmentValueMapById}
1272
+ onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
1273
+ onFocus={(id) => setTitleTextAreaId(id)}
1274
+ varRegex={rcsVarRegex}
1275
+ placeholderPrefix=""
1276
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
1277
+ />
1159
1278
  ) : (
1279
+ <div className="rcs_text_area_wrapper">
1160
1280
  <CapInput
1161
1281
  className={`rcs-template-title-input ${
1162
1282
  !isTemplateApproved ? "rcs-edit-disabled" : ""
@@ -1169,8 +1289,8 @@ const splitTemplateVarString = (str) => {
1169
1289
  errorMessage={templateTitleError}
1170
1290
  disabled={isEditFlow || !isFullMode}
1171
1291
  />
1292
+ </div>
1172
1293
  )}
1173
- </div>
1174
1294
  {(isEditFlow || !isFullMode) && templateTitleError && (
1175
1295
  <CapError className="rcs-template-title-error">
1176
1296
  {templateTitleError}
@@ -1218,7 +1338,18 @@ const splitTemplateVarString = (str) => {
1218
1338
  <CapRow className="rcs-create-template-message-input">
1219
1339
  <div className="rcs_text_area_wrapper">
1220
1340
  {(isEditFlow || !isFullMode)
1221
- ? renderedRCSEditMessage(splitTemplateVarString(templateDesc), MESSAGE_TEXT)
1341
+ ? (
1342
+ <VarSegmentMessageEditor
1343
+ key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
1344
+ templateString={templateDesc}
1345
+ valueMap={descriptionVarSegmentValueMapById}
1346
+ onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
1347
+ onFocus={(id) => setDescTextAreaId(id)}
1348
+ varRegex={rcsVarRegex}
1349
+ placeholderPrefix=""
1350
+ getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
1351
+ />
1352
+ )
1222
1353
  : (
1223
1354
  <>
1224
1355
  <TextArea
@@ -1240,7 +1371,7 @@ const splitTemplateVarString = (str) => {
1240
1371
  data-testid="rcs_text_area"
1241
1372
  disabled={(isEditFlow || !isFullMode)}
1242
1373
  />
1243
- {!isAiContentBotDisabled && !isEditFlow && (
1374
+ {!aiContentBotDisabled && !isEditFlow && (
1244
1375
  <CapAskAira.ContentGenerationBot
1245
1376
  text={templateDesc || ""}
1246
1377
  setText={(text) => onTemplateDescChange({ target: { value: text } })}
@@ -1262,7 +1393,9 @@ const splitTemplateVarString = (str) => {
1262
1393
  {templateDescError}
1263
1394
  </CapError>
1264
1395
  )}
1265
- {!isEditFlow && isFullMode && renderDescriptionCharacterCount()}
1396
+ {(isEditFlow || !isFullMode)
1397
+ ? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
1398
+ : (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
1266
1399
  {!isFullMode && hasTag() && (
1267
1400
  <CapAlert
1268
1401
  message={
@@ -1282,18 +1415,6 @@ const splitTemplateVarString = (str) => {
1282
1415
  );
1283
1416
  };
1284
1417
 
1285
-
1286
- const fallbackSmsLength = () => (
1287
- <CapLabel type="label1" className="fallback-sms-length">
1288
- {formatMessage(messages.totalCharacters, {
1289
- smsCount: Math.ceil(
1290
- fallbackMessage?.length / FALLBACK_MESSAGE_MAX_LENGTH,
1291
- ),
1292
- number: fallbackMessage?.length,
1293
- })}
1294
- </CapLabel>
1295
- );
1296
-
1297
1418
  // Get character count for title (rich card only)
1298
1419
  const getTitleCharacterCount = () => {
1299
1420
  if (templateType === contentType.text_message) return 0;
@@ -1353,7 +1474,7 @@ const splitTemplateVarString = (str) => {
1353
1474
  const hasTag = () => {
1354
1475
  // Check cardVarMapped values for tags
1355
1476
  if (cardVarMapped && Object.keys(cardVarMapped).length > 0) {
1356
- const hasTagInMapped = Object.values(cardVarMapped).some(value =>
1477
+ const hasTagInMapped = Object.values(cardVarMapped).some((value) =>
1357
1478
  isTagIncluded(value)
1358
1479
  );
1359
1480
  if (hasTagInMapped) return true;
@@ -1371,14 +1492,6 @@ const splitTemplateVarString = (str) => {
1371
1492
  return false;
1372
1493
  };
1373
1494
 
1374
- //adding creative dlt fallback sms handlers
1375
- const addDltMsgHandler = () => {
1376
- setShowDltContainer(true);
1377
- setDltMode(RCS_DLT_MODE.TEMPLATES);
1378
- setDltEditData({});
1379
- setFallbackMessage('');
1380
- };
1381
-
1382
1495
  const closeDltContainerHandler = () => {
1383
1496
  setShowDltContainer(false);
1384
1497
  setDltMode('');
@@ -1401,68 +1514,17 @@ const splitTemplateVarString = (str) => {
1401
1514
  const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
1402
1515
  '',
1403
1516
  );
1517
+ const templateNameFromDlt = get(dltEditData, 'name', '')
1518
+ || get(tempData, 'versions.base.name', '')
1519
+ || '';
1404
1520
  closeDltContainerHandler();
1405
1521
  setDltEditData(tempData);
1406
- setFallbackMessage(fallMsg);
1407
- setFallbackMessageError(fallMsg?.length > FALLBACK_MESSAGE_MAX_LENGTH);
1408
- setShowDltCard(true);
1409
- };
1410
-
1411
- const rcsDltCardDeleteHandler = () => {
1412
- closeDltContainerHandler();
1413
- setDltEditData({});
1414
- setFallbackMessage('');
1415
- setFallbackMessageError(false);
1416
- setShowDltCard(false);
1417
- };
1418
-
1419
- const dltFallbackListingPreviewhandler = (data) => {
1420
- const {
1421
- 'updated-sms-editor': updatedSmsEditor = [],
1422
- 'sms-editor': smsEditor = '',
1423
- } = data.versions.base || {};
1424
- setFallbackPreviewmode(true);
1425
- setDltPreviewData(
1426
- updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
1427
- );
1428
- };
1429
-
1430
- const getDltContentCardList = (content, channel) => {
1431
- const extra = [
1432
- <CapIcon
1433
- type="edit"
1434
- style={{ marginRight: '8px' }}
1435
- onClick={() => rcsDltEditSelectHandler(dltEditData)}
1436
- />,
1437
- <CapDropdown
1438
- overlay={(
1439
- <CapMenu>
1440
- <>
1441
- <CapMenu.Item
1442
- className="ant-dropdown-menu-item"
1443
- onClick={() => setFallbackPreviewmode(true)}
1444
- >
1445
- {formatMessage(globalMessages.preview)}
1446
- </CapMenu.Item>
1447
- <CapMenu.Item
1448
- className="ant-dropdown-menu-item"
1449
- onClick={rcsDltCardDeleteHandler}
1450
- >
1451
- {formatMessage(globalMessages.delete)}
1452
- </CapMenu.Item>
1453
- </>
1454
- </CapMenu>
1455
- )}
1456
- >
1457
- <CapIcon type="more" />
1458
- </CapDropdown>,
1459
- ];
1460
- return {
1461
- title: channel,
1462
- content,
1463
- cardType: channel,
1464
- extra,
1465
- };
1522
+ const unicodeFromDlt = get(tempData, 'versions.base.unicode-validity');
1523
+ setSmsFallbackData({
1524
+ templateName: templateNameFromDlt,
1525
+ content: fallMsg,
1526
+ ...(typeof unicodeFromDlt === 'boolean' ? { unicodeValidity: unicodeFromDlt } : {}),
1527
+ });
1466
1528
  };
1467
1529
 
1468
1530
  const getDltSlideBoxContent = () => {
@@ -1489,7 +1551,6 @@ const splitTemplateVarString = (str) => {
1489
1551
  isFullMode={isFullMode}
1490
1552
  isDltFromRcs
1491
1553
  onSelectTemplate={rcsDltEditSelectHandler}
1492
- handlePeviewTemplate={dltFallbackListingPreviewhandler}
1493
1554
  />
1494
1555
  );
1495
1556
  } else if (dltMode === RCS_DLT_MODE.EDIT) {
@@ -1510,147 +1571,32 @@ const splitTemplateVarString = (str) => {
1510
1571
  return { dltHeader, dltContent };
1511
1572
  };
1512
1573
 
1513
- const renderFallBackSmsComponent = () => {
1514
- // Completely disable fallback functionality when DLT is disabled
1515
- return null;
1516
-
1517
- // const contentArr = [];
1518
- // const showAddCreativeBtnForDlt = isDltEnabled && !showDltCard;
1519
- // const showCardForDlt = isDltEnabled && showDltCard;
1520
- // const showNonDltFallbackComp = !showAddCreativeBtnForDlt && !showCardForDlt;
1521
- // //pushing common fallback sms headings
1522
- // contentArr.push(
1523
- // <CapRow
1524
- // style={{
1525
- // marginBottom: isDltEnabled ? '20px' : '10px',
1526
- // }}
1527
- // >
1528
- // <CapHeader
1529
- // title={(
1530
- // <CapRow type="flex">
1531
- // <CapHeading type="h4">
1532
- // {formatMessage(messages.fallbackLabel)}
1533
- // </CapHeading>
1534
- // <CapTooltipWithInfo
1535
- // placement="right"
1536
- // infoIconProps={{
1537
- // style: { marginLeft: '4px', marginTop: '3px' },
1538
- // }}
1539
- // title={formatMessage(messages.fallbackToolTip)}
1540
- // />
1541
- // </CapRow>
1542
- // )}
1543
- // description={formatMessage(messages.fallbackDesc)}
1544
- // suffix={
1545
- // isDltEnabled ? null : (
1546
- // <CapButton
1547
- // type="flat"
1548
- // className="fallback-preview-btn"
1549
- // prefix={<CapIcon type="eye" />}
1550
- // style={{ color: CAP_SECONDARY.base }}
1551
- // onClick={() => setFallbackPreviewmode(true)}
1552
- // disabled={fallbackMessage === '' || fallbackMessageError}
1553
- // >
1554
- // {formatMessage(globalMessages.preview)}
1555
- // </CapButton>
1556
- // )
1557
- // }
1558
- // />
1559
- // </CapRow>,
1560
- // );
1561
-
1562
- //dlt is enabled, and dlt content is not yet added, show button to add dlt creative
1563
- // showAddCreativeBtnForDlt
1564
- // && contentArr.push(
1565
- // <CapCard className="rcs-dlt-fallback-card">
1566
- // <CapRow type="flex" justify="center" align="middle">
1567
- // <CapColumn span={10}>
1568
- // <CapImage src={addCreativesIcon} />
1569
- // </CapColumn>
1570
- // <CapColumn span={14}>
1571
- // <CapButton
1572
- // className="add-dlt-btn"
1573
- // type="secondary"
1574
- // onClick={addDltMsgHandler}
1575
- // >
1576
- // {formatMessage(messages.addSmsCreative)}
1577
- // </CapButton>
1578
- // </CapColumn>
1579
- // </CapRow>
1580
- // </CapCard>,
1581
- // );
1582
-
1583
- // //dlt is enabled and dlt content is added, show it in a card
1584
- // showCardForDlt
1585
- // && contentArr.push(
1586
- // <CapCustomCardList
1587
- // cardList={[getDltContentCardList(fallbackMessage, SMS)]}
1588
- // className="rcs-dlt-card"
1589
- // />,
1590
- // fallbackMessageError && (
1591
- // <CapError className="rcs-fallback-len-error">
1592
- // {formatMessage(messages.fallbackMsgLenError)}
1593
- // </CapError>
1594
- // ),
1595
- // );
1596
-
1597
- // //dlt is not enabled, show non dlt text area
1598
- // showNonDltFallbackComp
1599
- // && contentArr.push(
1600
- // <>
1601
- // <CapRow>
1602
- // <CapHeader
1603
- // title={(
1604
- // <CapHeading type="h4">
1605
- // {formatMessage(messages.fallbackTextAreaLabel)}
1606
- // </CapHeading>
1607
- // )}
1608
- // suffix={(
1609
- // <TagList
1610
- // label={formatMessage(globalMessages.addLabels)}
1611
- // onTagSelect={onTagSelectFallback}
1612
- // location={location}
1613
- // tags={tags || []}
1614
- // onContextChange={handleOnTagsContextChange}
1615
- // injectedTags={injectedTags || {}}
1616
- // selectedOfferDetails={selectedOfferDetails}
1617
- // />
1618
- // )}
1619
- // />
1620
- // </CapRow>
1621
- // <div className="rcs_fallback_msg_textarea_wrapper">
1622
- // <TextArea
1623
- // id="rcs_fallback_message_textarea"
1624
- // autosize={{ minRows: 3, maxRows: 5 }}
1625
- // placeholder={formatMessage(messages.fallbackMsgPlaceholder)}
1626
- // onChange={onFallbackMessageChange}
1627
- // errorMessage={fallbackMessageError}
1628
- // value={fallbackMessage || ""}
1629
- // />
1630
- // {!isAiContentBotDisabled && (
1631
- // <CapAskAira.ContentGenerationBot
1632
- // text={fallbackMessage || ""}
1633
- // setText={(text) => {
1634
- // onFallbackMessageChange({ target: { value: text } });
1635
- // }}
1636
- // iconPlacement="float-br"
1637
- // rootStyle={{
1638
- // bottom: "0.5rem",
1639
- // right: "0.5rem",
1640
- // position: "absolute",
1641
- // }}
1642
- // />
1643
- // )}
1644
- // </div>
1645
- // <CapRow>{fallbackSmsLength()}</CapRow>
1646
- // </>
1647
- // );
1648
-
1649
- // return <>{contentArr}</>;
1650
- };
1574
+ const renderFallBackSmsComponent = () => (
1575
+ <SmsFallback
1576
+ value={smsFallbackData}
1577
+ onChange={setSmsFallbackData}
1578
+ parentLocation={location}
1579
+ smsRegister={smsRegister}
1580
+ isFullMode={isFullMode}
1581
+ selectedOfferDetails={selectedOfferDetails}
1582
+ channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
1583
+ sectionTitle={
1584
+ smsFallbackData
1585
+ ? formatMessage(messages.fallbackLabel)
1586
+ : formatMessage(messages.smsFallbackOptional)
1587
+ }
1588
+ templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
1589
+ templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
1590
+ /* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
1591
+ showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
1592
+ disableSelectTemplate={isEditFlow}
1593
+ eventContextTags={eventContextTags}
1594
+ onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
1595
+ isRcsEditFlow={isEditFlow}
1596
+ />
1597
+ );
1651
1598
 
1652
1599
  const uploadRcsImage = useCallback((file, type, fileParams, index) => {
1653
- setImageError(null);
1654
1600
  const isRcsThumbnail = index === 1;
1655
1601
  actions.uploadRcsAsset(file, type, {
1656
1602
  isRcsThumbnail,
@@ -1662,7 +1608,6 @@ const splitTemplateVarString = (str) => {
1662
1608
 
1663
1609
  const setUpdateRcsImageSrc = useCallback(
1664
1610
  (val) => {
1665
- setAssetListImage(val);
1666
1611
  updateRcsImageSrc(val);
1667
1612
  actions.clearRcsMediaAsset(0);
1668
1613
  },
@@ -1692,7 +1637,6 @@ const splitTemplateVarString = (str) => {
1692
1637
 
1693
1638
 
1694
1639
  const uploadRcsVideo = (file, type, fileParams) => {
1695
- setImageError(null);
1696
1640
  actions.uploadRcsAsset(file, type, {
1697
1641
  ...fileParams,
1698
1642
  type: 'video',
@@ -1701,9 +1645,6 @@ const splitTemplateVarString = (str) => {
1701
1645
  });
1702
1646
  };
1703
1647
 
1704
- const updateRcsVideoSrc = (val) => {
1705
- setRcsVideoSrc(val);
1706
- };
1707
1648
  const setUpdateRcsVideoSrc = useCallback((index, val) => {
1708
1649
  setRcsVideoSrc(val);
1709
1650
  setAssetList(val);
@@ -1732,7 +1673,7 @@ const splitTemplateVarString = (str) => {
1732
1673
  <>
1733
1674
  <CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
1734
1675
  <CapImageUpload
1735
- style={{ paddingTop: '20px' }}
1676
+ className="cap-custom-image-upload rcs-image-upload--top-spacing"
1736
1677
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1737
1678
  imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
1738
1679
  imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
@@ -1744,7 +1685,6 @@ const splitTemplateVarString = (str) => {
1744
1685
  updateOnReUpload={updateOnRcsThumbnailReUpload}
1745
1686
  minImgSize={RCS_THUMBNAIL_MIN_SIZE}
1746
1687
  index={1}
1747
- className="cap-custom-image-upload"
1748
1688
  key={`rcs-uploaded-image-${currentDimension}`}
1749
1689
  imageData={thumbnailData}
1750
1690
  channel={RCS}
@@ -1775,7 +1715,7 @@ const splitTemplateVarString = (str) => {
1775
1715
  value: dim.type,
1776
1716
  label: `${dim.label}`
1777
1717
  }))}
1778
- style={{ marginBottom: '20px' }}
1718
+ className="rcs-dimension-select--bottom-spacing"
1779
1719
  />
1780
1720
  </>
1781
1721
  )}
@@ -1789,7 +1729,6 @@ const splitTemplateVarString = (str) => {
1789
1729
  </div>
1790
1730
  ) : (
1791
1731
  <CapImageUpload
1792
- style={{ paddingTop: '20px' }}
1793
1732
  allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
1794
1733
  imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
1795
1734
  imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
@@ -1800,7 +1739,7 @@ const splitTemplateVarString = (str) => {
1800
1739
  updateImageSrc={setUpdateRcsImageSrc}
1801
1740
  updateOnReUpload={updateOnRcsImageReUpload}
1802
1741
  index={0}
1803
- className="cap-custom-image-upload"
1742
+ className="cap-custom-image-upload rcs-image-upload--top-spacing"
1804
1743
  key={`rcs-uploaded-image-${selectedDimension}`}
1805
1744
  imageData={rcsData}
1806
1745
  channel={RCS}
@@ -1830,7 +1769,7 @@ const splitTemplateVarString = (str) => {
1830
1769
  value: dim.type,
1831
1770
  label: `${dim.label}`
1832
1771
  }))}
1833
- style={{ marginBottom: '20px' }}
1772
+ className="rcs-dimension-select--bottom-spacing"
1834
1773
  />
1835
1774
  )}
1836
1775
  {(isEditFlow || !isFullMode) ? (
@@ -1890,8 +1829,16 @@ const splitTemplateVarString = (str) => {
1890
1829
  const getRcsPreview = () => {
1891
1830
 
1892
1831
  const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
1893
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1894
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
1832
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1833
+ const titleVarCountForResolve = isMediaTypeText
1834
+ ? 0
1835
+ : ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
1836
+ const resolvedTitle = isMediaTypeText
1837
+ ? ''
1838
+ : (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
1839
+ const resolvedDesc = isSlotMappingMode
1840
+ ? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
1841
+ : templateDesc;
1895
1842
  return (
1896
1843
  <UnifiedPreview
1897
1844
  channel={RCS}
@@ -1914,51 +1861,65 @@ const splitTemplateVarString = (str) => {
1914
1861
  );
1915
1862
  };
1916
1863
 
1917
- const getUnmappedDesc = (str, mapping) => {
1918
- if (!str) return '';
1919
- if (!mapping || Object.keys(mapping).length === 0) return str;
1920
- let result = str;
1921
- const replacements = [];
1922
- Object.entries(mapping).forEach(([key, value]) => {
1923
- const raw = (value ?? '').toString();
1924
- if (!raw || raw?.trim?.() === '') return;
1925
- const braced = /^\{\{[\s\S]*\}\}$/.test(raw) ? raw : `{{${raw}}}`;
1926
- replacements.push({ key, needle: raw });
1927
- if (braced !== raw) replacements.push({ key, needle: braced });
1928
- });
1929
- const seen = new Set();
1930
- const uniq = replacements
1931
- .filter(({ key, needle }) => {
1932
- const id = `${key}::${needle}`;
1933
- if (seen.has(id)) return false;
1934
- seen.add(id);
1935
- return true;
1936
- })
1937
- .sort((a, b) => (b.needle.length - a.needle.length));
1938
-
1939
- uniq.forEach(({ key, needle }) => {
1940
- if (!needle) return;
1941
- const escaped = needle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
1942
- const regex = new RegExp(escaped, 'g');
1943
- result = result.replace(regex, `{{${key}}}`);
1944
- });
1945
- return result;
1946
- };
1947
-
1948
1864
  const createPayload = () => {
1949
- const base = get(dltEditData, `versions.base`, {});
1950
- const {
1951
- template_id: templateId = '',
1952
- template_name = '',
1953
- 'sms-editor': template = '',
1954
- header: registeredSenderIds = [],
1955
- } = base;
1956
- const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1957
- const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
1865
+ const isSlotMappingMode = isEditFlow || !isFullMode;
1958
1866
  const alignment = isMediaTypeImage
1959
1867
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
1960
1868
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
1961
1869
 
1870
+ const heightTypeForCardWidth = isMediaTypeText
1871
+ ? undefined
1872
+ : isMediaTypeImage
1873
+ ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
1874
+ : isMediaTypeVideo
1875
+ ? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
1876
+ : undefined;
1877
+ const cardWidthFromSelection =
1878
+ heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
1879
+
1880
+ /** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
1881
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
1882
+ const smsFallbackMerged = !isFullMode
1883
+ ? (() => {
1884
+ const local =
1885
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
1886
+ return {
1887
+ ...smsFromApiShape,
1888
+ ...local,
1889
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
1890
+ };
1891
+ })()
1892
+ : (smsFallbackData || {});
1893
+ const smsFallbackForPayload = (() => {
1894
+ if (isFullMode) {
1895
+ return hasMeaningfulSmsFallbackShape(smsFallbackData) ? smsFallbackData : null;
1896
+ }
1897
+ const mapped = {
1898
+ templateName:
1899
+ smsFallbackMerged.templateName
1900
+ || smsFallbackMerged.smsTemplateName
1901
+ || '',
1902
+ // Use `||` so empty `content` does not block campaign/API `message` (common in embedded flows).
1903
+ content:
1904
+ smsFallbackMerged.content
1905
+ || smsFallbackMerged.smsContent
1906
+ || smsFallbackMerged.smsTemplateContent
1907
+ || smsFallbackMerged.message
1908
+ || '',
1909
+ templateContent:
1910
+ pickFirstSmsFallbackTemplateString(smsFallbackMerged)
1911
+ || '',
1912
+ ...(typeof smsFallbackMerged.unicodeValidity === 'boolean'
1913
+ && { unicodeValidity: smsFallbackMerged.unicodeValidity }),
1914
+ ...(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
1915
+ && Object.keys(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
1916
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
1917
+ smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
1918
+ }),
1919
+ };
1920
+ return hasMeaningfulSmsFallbackShape(mapped) ? mapped : null;
1921
+ })();
1922
+
1962
1923
  const payload = {
1963
1924
  name: templateName,
1964
1925
  versions: {
@@ -1970,12 +1931,14 @@ const splitTemplateVarString = (str) => {
1970
1931
  cardSettings: {
1971
1932
  cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
1972
1933
  ...(alignment && { mediaAlignment: alignment }),
1973
- cardWidth: SMALL,
1934
+ cardWidth: cardWidthFromSelection,
1974
1935
  },
1975
1936
  cardContent: [
1976
1937
  {
1977
- title: resolvedTitle,
1978
- description: resolvedDesc,
1938
+ // Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
1939
+ // / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
1940
+ title: templateTitle,
1941
+ description: templateDesc,
1979
1942
  mediaType: templateMediaType,
1980
1943
  ...(!isMediaTypeText && {media: {
1981
1944
  mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
@@ -1984,23 +1947,30 @@ const splitTemplateVarString = (str) => {
1984
1947
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
1985
1948
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
1986
1949
  }}),
1987
- ...(!isFullMode && (() => {
1988
- const tokens = [
1989
- ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
1990
- ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
1950
+ ...(isSlotMappingMode && (() => {
1951
+ const templateVarTokens = [
1952
+ ...(templateTitle?.match(rcsVarRegex) ?? []),
1953
+ ...(templateDesc?.match(rcsVarRegex) ?? []),
1991
1954
  ];
1992
- const allowedKeys = tokens
1993
- .map((t) => getVarNameFromToken(t))
1994
- .filter(Boolean);
1995
- const nextMap = {};
1996
- allowedKeys.forEach((k) => {
1997
- if (Object.prototype.hasOwnProperty.call(cardVarMapped || {}, k)) {
1998
- nextMap[k] = cardVarMapped[k];
1999
- } else {
2000
- nextMap[k] = '';
1955
+ const persistedSlotVarMap = {};
1956
+ const seenSemanticVarNames = new Set();
1957
+ templateVarTokens.forEach((token, slotIndexZeroBased) => {
1958
+ const varName = getVarNameFromToken(token);
1959
+ if (!varName) return;
1960
+ const resolvedRawValue = resolveCardVarMappedSlotValue(
1961
+ cardVarMapped,
1962
+ varName,
1963
+ slotIndexZeroBased,
1964
+ isSlotMappingMode,
1965
+ );
1966
+ const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
1967
+ persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
1968
+ if (!seenSemanticVarNames.has(varName)) {
1969
+ seenSemanticVarNames.add(varName);
1970
+ persistedSlotVarMap[varName] = sanitizedSlotValue;
2001
1971
  }
2002
1972
  });
2003
- return { cardVarMapped: nextMap };
1973
+ return { cardVarMapped: persistedSlotVarMap };
2004
1974
  })()),
2005
1975
  ...(suggestions.length > 0 && { suggestions }),
2006
1976
  }
@@ -2008,17 +1978,29 @@ const splitTemplateVarString = (str) => {
2008
1978
  contentType: isFullMode ? templateType : RICHCARD,
2009
1979
  ...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
2010
1980
  },
2011
- smsFallBackContent: {
2012
- message: fallbackMessage,
2013
- ...(isDltEnabled && {
2014
- templateConfigs: {
2015
- templateId,
2016
- templateName: template_name,
2017
- template,
2018
- registeredSenderIds,
1981
+ ...(smsFallbackForPayload && (() => {
1982
+ const smsBodyText =
1983
+ smsFallbackForPayload.content
1984
+ || smsFallbackForPayload.templateContent
1985
+ || smsFallbackForPayload.message
1986
+ || smsFallbackForPayload.smsContent
1987
+ || '';
1988
+ return {
1989
+ smsFallBackContent: {
1990
+ smsTemplateName: smsFallbackForPayload.templateName || '',
1991
+ smsContent: smsBodyText,
1992
+ // cap-campaigns-v2 `normalizeRcsMessageContentForApi` only serializes `message` (+ templateConfigs); without this key SMS fallback is dropped on send.
1993
+ message: smsBodyText,
1994
+ ...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
1995
+ unicodeValidity: smsFallbackForPayload.unicodeValidity,
1996
+ }),
1997
+ ...(smsFallbackForPayload.rcsSmsFallbackVarMapped
1998
+ && Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
1999
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
2000
+ }),
2019
2001
  },
2020
- }),
2021
- },
2002
+ };
2003
+ })()),
2022
2004
  },
2023
2005
  },
2024
2006
  },
@@ -2028,6 +2010,84 @@ const splitTemplateVarString = (str) => {
2028
2010
  return payload;
2029
2011
  };
2030
2012
 
2013
+ /** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
2014
+ const testPreviewFormData = useMemo(() => {
2015
+ const payload = createPayload();
2016
+ const rcs = payload?.versions?.base?.content?.RCS;
2017
+ if (!rcs) return null;
2018
+ // createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
2019
+ const accountIdForCreateMessageMeta =
2020
+ (wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
2021
+ ? String(wecrmAccountId)
2022
+ : accountId;
2023
+ const rcsForTest = {
2024
+ ...rcs,
2025
+ rcsContent: {
2026
+ ...rcs.rcsContent,
2027
+ ...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
2028
+ },
2029
+ };
2030
+ const out = {
2031
+ versions: {
2032
+ base: {
2033
+ content: {
2034
+ RCS: rcsForTest,
2035
+ },
2036
+ },
2037
+ },
2038
+ };
2039
+ const fb = smsFallbackData;
2040
+ if (fb && (fb.smsTemplateId || fb.templateContent || fb.content)) {
2041
+ out.templateConfigs = {
2042
+ templateId: fb.smsTemplateId || '',
2043
+ template: fb.templateContent || fb.content || '',
2044
+ traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
2045
+ registeredSenderIds: Array.isArray(fb.registeredSenderIds) ? fb.registeredSenderIds : [],
2046
+ };
2047
+ }
2048
+ return out;
2049
+ }, [
2050
+ templateName,
2051
+ templateTitle,
2052
+ templateDesc,
2053
+ templateMediaType,
2054
+ cardVarMapped,
2055
+ suggestions,
2056
+ rcsImageSrc,
2057
+ rcsVideoSrc,
2058
+ rcsThumbnailSrc,
2059
+ selectedDimension,
2060
+ smsFallbackData,
2061
+ isFullMode,
2062
+ isEditFlow,
2063
+ templateType,
2064
+ accountId,
2065
+ wecrmAccountId,
2066
+ accessToken,
2067
+ accountName,
2068
+ hostName,
2069
+ smsRegister,
2070
+ ]);
2071
+
2072
+ /**
2073
+ * Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
2074
+ * with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
2075
+ * miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
2076
+ */
2077
+ const librarySmsFallbackMergedForValidation = useMemo(() => {
2078
+ if (isFullMode) {
2079
+ return smsFallbackData;
2080
+ }
2081
+ const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
2082
+ const local =
2083
+ smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
2084
+ return {
2085
+ ...smsFromApiShape,
2086
+ ...local,
2087
+ rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
2088
+ };
2089
+ }, [isFullMode, templateData, smsFallbackData]);
2090
+
2031
2091
  const actionCallback = ({ errorMessage, resp }, isEdit) => {
2032
2092
  // eslint-disable-next-line no-undef
2033
2093
  const error = errorMessage?.message || errorMessage;
@@ -2057,6 +2117,9 @@ const splitTemplateVarString = (str) => {
2057
2117
  _id: params?.id,
2058
2118
  validity: true,
2059
2119
  type: RCS,
2120
+ // CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
2121
+ // the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
2122
+ closeSlideBoxAfterSubmit: !isFullMode,
2060
2123
  };
2061
2124
  getFormData(formDataParams);
2062
2125
  };
@@ -2070,6 +2133,7 @@ const splitTemplateVarString = (str) => {
2070
2133
  actionCallback({ resp, errorMessage });
2071
2134
  setSpin(false); // Always turn off spinner
2072
2135
  if (!errorMessage) {
2136
+ setTemplateStatus(RCS_STATUSES.pending);
2073
2137
  onCreateComplete();
2074
2138
  }
2075
2139
  });
@@ -2081,6 +2145,64 @@ const splitTemplateVarString = (str) => {
2081
2145
  }
2082
2146
  };
2083
2147
 
2148
+ /** When a fallback SMS row exists, require non-empty body (trimmed) and filled var slots (DLT). */
2149
+ const smsFallbackBlocksDone = () => {
2150
+ // Non-DLT library: user removed SMS fallback (local null) but template still carries fallback — block Done.
2151
+ if (
2152
+ !isFullMode
2153
+ && !isTraiDLTEnable(isFullMode, smsRegister)
2154
+ && smsFallbackData == null
2155
+ && hasMeaningfulSmsFallbackShape(
2156
+ getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
2157
+ )
2158
+ ) {
2159
+ return true;
2160
+ }
2161
+ if (!smsFallbackData) return false;
2162
+ // Full-mode (Send for approval): SMS fallback is optional. Tag-slot mapping is a display/preview
2163
+ // concern, not a structural requirement for approval — the registered SMS template body stands on
2164
+ // its own. Never block the Send for approval button due to missing or unfilled fallback var slots.
2165
+ if (isFullMode) return false;
2166
+ const merged = librarySmsFallbackMergedForValidation;
2167
+ const templateText = pickFirstSmsFallbackTemplateString(merged);
2168
+ if (!templateText) {
2169
+ return true;
2170
+ }
2171
+ const rawVarMap =
2172
+ merged.rcsSmsFallbackVarMapped
2173
+ || merged['rcs-sms-fallback-var-mapped'];
2174
+ const varMap =
2175
+ rawVarMap != null && typeof rawVarMap === 'object' ? rawVarMap : {};
2176
+ return !areAllRcsSmsFallbackVarSlotsFilled(templateText, varMap);
2177
+ };
2178
+
2179
+ /**
2180
+ * Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
2181
+ * semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
2182
+ * / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
2183
+ */
2184
+ const isLibraryCampaignCardVarMappingIncomplete = () => {
2185
+ if (isFullMode) return false;
2186
+ const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
2187
+ rcsVarTestRegex.test(elem),
2188
+ );
2189
+ const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
2190
+ rcsVarTestRegex.test(elem),
2191
+ );
2192
+ const orderedVarNames = [
2193
+ ...titleTokens.map((t) => t.replace(/^\{\{|\}\}$/g, '')),
2194
+ ...descTokens.map((t) => t.replace(/^\{\{|\}\}$/g, '')),
2195
+ ];
2196
+ if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
2197
+ return true;
2198
+ }
2199
+ return orderedVarNames.some((name, globalIdx) => {
2200
+ const v = resolveCardVarMappedSlotValue(cardVarMapped, name, globalIdx, true);
2201
+ const s = v == null ? '' : String(v);
2202
+ return s.trim() === '';
2203
+ });
2204
+ };
2205
+
2084
2206
  const isDisableDone = () => {
2085
2207
  if(isEditFlow){
2086
2208
  return false;
@@ -2091,40 +2213,16 @@ const splitTemplateVarString = (str) => {
2091
2213
  }
2092
2214
  }
2093
2215
 
2094
- if(!isFullMode){
2095
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2096
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2097
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2098
-
2099
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2100
- return true;
2101
- }
2102
-
2103
- const hasEmptyMapping =
2104
- cardVarMapped &&
2105
- Object.keys(cardVarMapped).length > 0 &&
2106
- Object.entries(cardVarMapped).some(([_, v]) => {
2107
- if (typeof v !== 'string') return !v; // null/undefined
2108
- return v.trim() === ''; // empty string
2109
- });
2110
-
2111
- if (hasEmptyMapping) {
2112
- return true;
2113
- }
2114
-
2115
- const anyMissing = allVars.some(name => {
2116
- const v = cardVarMapped?.[name];
2117
- if (typeof v !== 'string') return !v;
2118
- return v.trim() === '';
2119
- });
2120
- if (anyMissing) {
2121
- return true;
2122
- }
2216
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
2217
+ return true;
2123
2218
  }
2124
2219
 
2125
- if (isMediaTypeText && templateDesc.trim() === '') {
2220
+ if (smsFallbackBlocksDone()) {
2126
2221
  return true;
2222
+ }
2127
2223
 
2224
+ if (isMediaTypeText && templateDesc.trim() === '') {
2225
+ return true;
2128
2226
  }
2129
2227
  if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
2130
2228
  return true;
@@ -2142,53 +2240,36 @@ const splitTemplateVarString = (str) => {
2142
2240
  return true;
2143
2241
  }
2144
2242
  }
2145
- if (templateDescError || templateTitleError || fallbackMessageError) {
2243
+ if (templateDescError || templateTitleError) {
2244
+ return true;
2245
+ }
2246
+ if (
2247
+ smsFallbackData?.content
2248
+ && smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
2249
+ ) {
2146
2250
  return true;
2147
2251
  }
2148
2252
  return false;
2149
2253
  };
2150
2254
 
2151
2255
  const isEditDisableDone = () => {
2152
-
2153
2256
  if (templateStatus !== RCS_STATUSES.approved) {
2154
2257
  return true;
2155
2258
  }
2156
2259
 
2157
- if (!isFullMode) {
2158
- if (templateName.trim() === '' || templateNameError) {
2159
- return true;
2160
- }
2260
+ // if (!isFullMode) {
2261
+ // if (templateName.trim() === '' || templateNameError) {
2262
+ // return true;
2263
+ // }
2264
+ // }
2265
+ if (isLibraryCampaignCardVarMappingIncomplete()) {
2266
+ return true;
2161
2267
  }
2162
- if(!isFullMode){
2163
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2164
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
2165
- const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
2166
-
2167
- if (allVars.length > 0 && isEmpty(cardVarMapped)) {
2168
- return true;
2169
- }
2170
-
2171
- const hasEmptyMapping =
2172
- cardVarMapped &&
2173
- Object.keys(cardVarMapped).length > 0 &&
2174
- Object.entries(cardVarMapped).some(([_, v]) => {
2175
- if (typeof v !== 'string') return !v; // null/undefined
2176
- return v.trim() === ''; // empty string
2177
- });
2178
2268
 
2179
- if (hasEmptyMapping) {
2180
- return true;
2181
- }
2182
-
2183
- const anyMissing = allVars.some(name => {
2184
- const v = cardVarMapped?.[name];
2185
- if (typeof v !== 'string') return !v;
2186
- return v.trim() === '';
2187
- });
2188
- if (anyMissing) {
2189
- return true;
2190
- }
2269
+ if (smsFallbackBlocksDone()) {
2270
+ return true;
2191
2271
  }
2272
+
2192
2273
  if (isMediaTypeText && templateDesc.trim() === '') {
2193
2274
  return true;
2194
2275
  }
@@ -2207,7 +2288,13 @@ const splitTemplateVarString = (str) => {
2207
2288
  return true;
2208
2289
  }
2209
2290
  }
2210
- if (templateTitleError || templateDescError || fallbackMessageError) {
2291
+ if (templateTitleError || templateDescError) {
2292
+ return true;
2293
+ }
2294
+ if (
2295
+ smsFallbackData?.content
2296
+ && smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
2297
+ ) {
2211
2298
  return true;
2212
2299
  }
2213
2300
  return false;
@@ -2257,52 +2344,56 @@ const splitTemplateVarString = (str) => {
2257
2344
  };
2258
2345
 
2259
2346
  const getMainContent = () => {
2260
- if (showDltContainer && !fallbackPreviewmode) {
2261
- const dltSlideBoxContent = showDltContainer && getDltSlideBoxContent();
2262
- const { dltHeader = '', dltContent = '' } = dltSlideBoxContent;
2263
- return (
2264
- <CapSlideBox
2265
- show={showDltContainer}
2266
- header={dltHeader}
2267
- content={dltContent}
2268
- handleClose={closeDltContainerHandler}
2269
- size="size-xl"
2270
- />
2271
- );
2272
- }
2347
+ // Slideboxes are rendered outside the page-level spinner to avoid
2348
+ // stacking/blur issues during initial loads.
2349
+ if (showDltContainer) return null;
2273
2350
 
2274
2351
  return (
2275
2352
  <>
2276
- {templateStatus !== '' && (<CapRow className="template-status-container">
2277
- <CapLabel type="label2">
2278
- {formatMessage(messages.templateStatusLabel)}
2279
- </CapLabel>
2280
-
2281
- {templateStatus && (
2282
- <CapAlert
2283
- message={getTemplateStatusMessage()}
2284
- type={getTemplateStatusType(templateStatus)}
2285
- />
2286
- )}
2287
- </CapRow>
2353
+ {templateStatus !== '' && (
2354
+ <CapRow className="template-status-container">
2355
+ <CapColumn span={14}>
2356
+ <CapLabel type="label2">
2357
+ {formatMessage(messages.templateStatusLabel)}
2358
+ </CapLabel>
2359
+
2360
+ {templateStatus && (
2361
+ <CapAlert
2362
+ message={getTemplateStatusMessage()}
2363
+ type={getTemplateStatusType(templateStatus)}
2364
+ />
2365
+ )}
2366
+ </CapColumn>
2367
+ </CapRow>
2288
2368
  )}
2289
- <CapRow className="cap-rcs-creatives">
2369
+ <CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
2290
2370
  <CapColumn span={14}>
2291
2371
  {/* template name */}
2292
2372
  {isFullMode && (
2293
- <CapInput
2294
- id="rcs_template_name_input"
2295
- data-testid="template_name"
2296
- onChange={onTemplateNameChange}
2297
- errorMessage={templateNameError}
2298
- placeholder={formatMessage(
2299
- globalMessages.templateNamePlaceholder,
2300
- )}
2301
- value={templateName || ''}
2302
- size="default"
2303
- label={formatMessage(globalMessages.creativeNameLabel)}
2304
- disabled={(isEditFlow || !isFullMode)}
2305
- />
2373
+ isEditFlow ? (
2374
+ <div className="rcs-creative-name-readonly">
2375
+ <CapHeading type="h4">
2376
+ {formatMessage(globalMessages.creativeNameLabel)}
2377
+ </CapHeading>
2378
+ <CapHeading type="h5" className="rcs-creative-name-value">
2379
+ {templateName || '-'}
2380
+ </CapHeading>
2381
+ </div>
2382
+ ) : (
2383
+ <CapInput
2384
+ id="rcs_template_name_input"
2385
+ data-testid="template_name"
2386
+ onChange={onTemplateNameChange}
2387
+ errorMessage={templateNameError}
2388
+ placeholder={formatMessage(
2389
+ globalMessages.templateNamePlaceholder,
2390
+ )}
2391
+ value={templateName || ''}
2392
+ size="default"
2393
+ label={formatMessage(globalMessages.creativeNameLabel)}
2394
+ disabled={(isEditFlow || !isFullMode)}
2395
+ />
2396
+ )
2306
2397
  )}
2307
2398
  {renderLabel('templateTypeLabel')}
2308
2399
  <CapRadioGroup
@@ -2330,7 +2421,7 @@ const splitTemplateVarString = (str) => {
2330
2421
  </>
2331
2422
  )}
2332
2423
  {renderTextComponent()}
2333
- <CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
2424
+ <CapDivider className="rcs-fallback-section-divider" />
2334
2425
  {renderFallBackSmsComponent()}
2335
2426
  <div className="rcs-scroll-div" />
2336
2427
  </CapColumn>
@@ -2342,7 +2433,8 @@ const splitTemplateVarString = (str) => {
2342
2433
 
2343
2434
 
2344
2435
  <div className="rcs-footer">
2345
- {!isEditFlow && (
2436
+ {/* 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). */}
2437
+ {!isEditFlow && isFullMode && (
2346
2438
  <>
2347
2439
  <div className="button-disabled-tooltip-wrapper">
2348
2440
  <CapButton
@@ -2363,7 +2455,6 @@ const splitTemplateVarString = (str) => {
2363
2455
  className="rcs-test-preview-btn"
2364
2456
  type="secondary"
2365
2457
  disabled={true}
2366
- style={{ marginLeft: "8px" }}
2367
2458
  >
2368
2459
  <FormattedMessage {...creativesMessages.testAndPreview} />
2369
2460
  </CapButton>
@@ -2396,51 +2487,6 @@ const splitTemplateVarString = (str) => {
2396
2487
  </>
2397
2488
  )}
2398
2489
  </div>
2399
-
2400
-
2401
- {fallbackPreviewmode && (
2402
- <CapSlideBox
2403
- className="rcs-fallback-preview"
2404
- show={fallbackPreviewmode}
2405
- header={(
2406
- <CapHeading type="h7" style={{ color: CAP_G01 }}>
2407
- {formatMessage(messages.fallbackPreviewtitle)}
2408
- </CapHeading>
2409
- )}
2410
- content={(
2411
- <>
2412
- <UnifiedPreview
2413
- channel={RCS}
2414
- content={{
2415
- rcsPreviewContent: {
2416
- rcsDesc: tempMsg,
2417
- },
2418
- }}
2419
- device={ANDROID}
2420
- showDeviceToggle={false}
2421
- showHeader={false}
2422
- formatMessage={formatMessage}
2423
- />
2424
- <CapHeading
2425
- type="h3"
2426
- style={{ textAlign: 'center' }}
2427
- className="margin-t-16"
2428
- >
2429
- {formatMessage(messages.totalCharacters, {
2430
- smsCount: Math.ceil(
2431
- tempMsg?.length / FALLBACK_MESSAGE_MAX_LENGTH,
2432
- ),
2433
- number: tempMsg.length,
2434
- })}
2435
- </CapHeading>
2436
- </>
2437
- )}
2438
- handleClose={() => {
2439
- setFallbackPreviewmode(false);
2440
- setDltPreviewData('');
2441
- }}
2442
- />
2443
- )}
2444
2490
  </>
2445
2491
  );
2446
2492
  };
@@ -2449,12 +2495,38 @@ const splitTemplateVarString = (str) => {
2449
2495
  <CapSpin spinning={loadingTags || spin}>
2450
2496
  {getMainContent()}
2451
2497
  </CapSpin>
2498
+
2499
+ {showDltContainer && (() => {
2500
+ const { dltHeader = '', dltContent = '' } = getDltSlideBoxContent() || {};
2501
+ return (
2502
+ <CapSlideBox
2503
+ show={showDltContainer}
2504
+ header={dltHeader}
2505
+ content={dltContent}
2506
+ handleClose={closeDltContainerHandler}
2507
+ size="size-xl"
2508
+ />
2509
+ );
2510
+ })()}
2511
+
2452
2512
  <TestAndPreviewSlidebox
2453
2513
  show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
2454
2514
  onClose={handleCloseTestAndPreview}
2455
- formData={null} // RCS doesn't use formData structure like SMS
2456
- content={getTemplateContent()}
2515
+ formData={testPreviewFormData}
2516
+ content={testAndPreviewContent}
2457
2517
  currentChannel={RCS}
2518
+ orgUnitId={orgUnitId}
2519
+ smsFallbackContent={
2520
+ smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
2521
+ ? {
2522
+ templateContent:
2523
+ smsFallbackData.templateContent || smsFallbackData.content || '',
2524
+ templateName: smsFallbackData.templateName || '',
2525
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackData?.rcsSmsFallbackVarMapped ?? {},
2526
+ }
2527
+ : null
2528
+ }
2529
+ smsRegister={smsRegister}
2458
2530
  />
2459
2531
  </>
2460
2532
  );
@@ -2466,7 +2538,6 @@ const mapStateToProps = createStructuredSelector({
2466
2538
  metaEntities: makeSelectMetaEntities(),
2467
2539
  loadingTags: isLoadingMetaEntities(),
2468
2540
  injectedTags: setInjectedTags(),
2469
- currentOrgDetails: selectCurrentOrgDetails(),
2470
2541
  });
2471
2542
 
2472
2543
  const mapDispatchToProps = (dispatch) => ({