@capillarytech/creatives-library 8.0.318 → 8.0.320

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 (139) hide show
  1. package/constants/unified.js +15 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/utils/templateVarUtils.js +172 -0
  7. package/utils/tests/templateVarUtils.test.js +160 -0
  8. package/v2Components/CapTagList/index.js +10 -0
  9. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  16. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  19. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  20. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  21. package/v2Components/CommonTestAndPreview/index.js +693 -155
  22. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  23. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  24. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  25. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  26. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  27. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  29. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  30. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  31. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  32. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  33. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  34. package/v2Components/FormBuilder/index.js +7 -1
  35. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  36. package/v2Components/SmsFallback/constants.js +73 -0
  37. package/v2Components/SmsFallback/index.js +956 -0
  38. package/v2Components/SmsFallback/index.scss +265 -0
  39. package/v2Components/SmsFallback/messages.js +78 -0
  40. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  41. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  42. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  43. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  44. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  45. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  46. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  47. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  48. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  49. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  50. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  51. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  52. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  53. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  54. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  55. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  56. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  57. package/v2Containers/CommunicationFlow/constants.js +200 -0
  58. package/v2Containers/CommunicationFlow/index.js +102 -0
  59. package/v2Containers/CommunicationFlow/messages.js +346 -0
  60. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  61. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  62. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  63. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  64. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  65. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  66. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  67. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  68. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  69. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  70. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  71. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  72. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  75. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  76. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  77. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  78. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  79. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  80. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  81. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  82. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  83. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  84. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  85. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  86. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  87. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  88. package/v2Containers/CreativesContainer/constants.js +12 -0
  89. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  90. package/v2Containers/CreativesContainer/index.js +289 -99
  91. package/v2Containers/CreativesContainer/index.scss +51 -1
  92. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  93. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  94. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  95. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  96. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  97. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  98. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  99. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  100. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  101. package/v2Containers/Rcs/constants.js +32 -1
  102. package/v2Containers/Rcs/index.js +950 -873
  103. package/v2Containers/Rcs/index.scss +85 -6
  104. package/v2Containers/Rcs/messages.js +10 -1
  105. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  106. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  107. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  108. package/v2Containers/Rcs/tests/index.test.js +41 -38
  109. package/v2Containers/Rcs/tests/mockData.js +38 -0
  110. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  111. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  112. package/v2Containers/Rcs/utils.js +358 -10
  113. package/v2Containers/Sms/Create/index.js +81 -36
  114. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  115. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  116. package/v2Containers/SmsTrai/Create/index.js +9 -4
  117. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  118. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  119. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  120. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  121. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  122. package/v2Containers/SmsWrapper/index.js +37 -8
  123. package/v2Containers/TagList/index.js +6 -0
  124. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  125. package/v2Containers/Templates/_templates.scss +61 -2
  126. package/v2Containers/Templates/actions.js +11 -0
  127. package/v2Containers/Templates/constants.js +2 -0
  128. package/v2Containers/Templates/index.js +90 -40
  129. package/v2Containers/Templates/sagas.js +57 -12
  130. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  131. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  132. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  133. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  134. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  135. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  136. package/v2Containers/TemplatesV2/index.js +86 -23
  137. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  138. package/v2Containers/Whatsapp/index.js +3 -20
  139. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -1,10 +1,11 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
2
  /* eslint-disable no-undef */
3
- import React, { useState, useEffect } from 'react';
3
+ import React, { useState, useEffect, useMemo, useRef } from 'react';
4
+ import PropTypes from 'prop-types';
4
5
  import { createStructuredSelector } from 'reselect';
5
6
  import { bindActionCreators } from 'redux';
6
7
  import { FormattedMessage } from 'react-intl';
7
- import { get, cloneDeep, isEmpty, isObject } from 'lodash';
8
+ import { get, cloneDeep, isEmpty } from 'lodash';
8
9
  import styled from 'styled-components';
9
10
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
10
11
  import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
@@ -27,13 +28,13 @@ import {
27
28
  CAP_SPACE_32,
28
29
  CAP_SPACE_04,
29
30
  CAP_WHITE,
30
- CAP_G10,
31
31
  } from '@capillarytech/cap-ui-library/styled/variables';
32
32
  import { makeSelectTemplateDetailsResponse } from '../../Sms/Edit/selectors';
33
33
  import { makeSelectMetaEntities, selectLiquidStateDetails, setInjectedTags } from '../../Cap/selectors';
34
34
  import * as smsEditActions from '../../Sms/Edit/actions';
35
35
  import * as globalActions from '../../Cap/actions';
36
36
  import messages from './messages';
37
+ import globalMessages from '../../Cap/messages';
37
38
  import TagList from '../../TagList';
38
39
  import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
39
40
  import UnifiedPreview from '../../../v2Components/CommonTestAndPreview/UnifiedPreview';
@@ -41,6 +42,8 @@ import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox
41
42
  import withCreatives from '../../../hoc/withCreatives';
42
43
  import {
43
44
  CHARLIMIT,
45
+ SMS,
46
+ SMS_TRAI_CONTENT_MAX_LENGTH,
44
47
  SMS_TRAI_VAR,
45
48
  TAG,
46
49
  EMBEDDED,
@@ -49,24 +52,39 @@ import {
49
52
  ALL,
50
53
  LIBRARY,
51
54
  } from './constants';
52
- import { SMS } from '../../CreativesContainer/constants';
53
55
  import v2EditSmsReducer from '../../Sms/Edit/reducer';
54
56
  import { v2SmsEditSagas } from '../../Sms/Edit/sagas';
55
57
  import ErrorInfoNote from '../../../v2Components/ErrorInfoNote';
58
+ import { isTraiDLTEnable, hasTraiDltFeature } from '../../../utils/common';
56
59
  import { validateLiquidTemplateContent } from '../../../utils/commonUtils';
57
60
  import { validateTags } from '../../../utils/tagValidations';
58
- import globalMessages from '../../Cap/messages';
59
61
  import { ANDROID } from '../../../v2Components/CommonTestAndPreview/constants';
62
+ import {
63
+ getFallbackResolvedContent,
64
+ splitTemplateVarString,
65
+ COMBINED_SMS_TEMPLATE_VAR_REGEX,
66
+ isAnyTemplateVarToken,
67
+ isDltHashVarToken,
68
+ } from '../../../utils/templateVarUtils';
69
+ import VarSegmentMessageEditor from '../../../v2Components/VarSegmentMessageEditor';
70
+ import rcsMessages from '../../Rcs/messages';
71
+
72
+ import './index.scss';
60
73
 
61
- let varMap = {};
62
- let traiData = {};
63
74
  const { TextArea } = CapInput;
64
75
  const { CapLabelInline } = CapLabel;
65
76
 
77
+ /** Redux `metaEntities` may be an Immutable.Map; TagList needs plain `tags.standard`. */
78
+ const getStandardTagsFromMeta = (metaEntities) => {
79
+ if (!metaEntities) return [];
80
+ const plain = typeof metaEntities.toJS === 'function' ? metaEntities.toJS() : metaEntities;
81
+ const standard = get(plain, 'tags.standard');
82
+ return Array.isArray(standard) ? standard : [];
83
+ };
84
+
66
85
  export const SmsTraiEdit = (props) => {
67
86
  const {
68
87
  intl,
69
- handleClose,
70
88
  params,
71
89
  actions,
72
90
  templateDetails,
@@ -85,6 +103,17 @@ export const SmsTraiEdit = (props) => {
85
103
  fetchingLiquidTags,
86
104
  getLiquidTags,
87
105
  showLiquidErrorInFooter,
106
+ smsRegister,
107
+ // RCS -> SMS fallback edit mode
108
+ isRcsSmsFallback = false,
109
+ /** When editing an existing RCS template, lock Unicode (matches product: no mid-edit toggle). */
110
+ isRcsEditFlow = false,
111
+ showPreviewInRcsFallback = false,
112
+ hidePreview = false,
113
+ isOverview = false,
114
+ forceFullTagContext = false,
115
+ /** RCS parent: merge `rcsSmsFallbackVarMapped` into `smsFallbackData` (same idea as `cardVarMapped`). */
116
+ onRcsFallbackEditorStateChange,
88
117
  } = props || {};
89
118
 
90
119
  const { formatMessage } = intl;
@@ -94,14 +123,149 @@ export const SmsTraiEdit = (props) => {
94
123
  const [tags, updateTags] = useState([]);
95
124
  const [textAreaId, updateTextAreaId] = useState();
96
125
  const [isValidationError, updateIsValidationError] = useState(false);
126
+ const [isTagValidationError, updateIsTagValidationError] = useState(false);
97
127
  const [totalMessageLength, setTotalMessageLength] = useState(0);
98
128
  const [isUnicodeAllowed, updateIsUnicodeAllowed] = useState(true);
129
+ const [fallbackText, setFallbackText] = useState('');
130
+ const [fallbackVarMappedData, setFallbackVarMappedData] = useState({});
131
+ const [fallbackFocusedId, setFallbackFocusedId] = useState('');
99
132
  const [showMsgLengthNote, updateshowMsgLengthNote] = useState(false);
100
133
  const [liquidErrorMessages, setLiquidErrorMessages] = useState({});
101
134
  const [isLiquidValidationError, setIsLiquidValidationError] = useState(false);
102
135
  /** After user closes the validation panel, keep it hidden until Save is clicked again (even if ErrorInfoNote remounts). */
103
136
  const [liquidErrorPanelDismissed, setLiquidErrorPanelDismissed] = useState(false);
104
137
  const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
138
+
139
+ /** Per-instance only — was module-level (leaked across mounts / multiple editors). */
140
+ const varMapRef = useRef({});
141
+ const traiDataRef = useRef({});
142
+ const tagValidationResponseRef = useRef({});
143
+ /** De-dupe repeated /meta/TAG fetches from popover/context callbacks. */
144
+ const lastTagSchemaQueryKeyRef = useRef(null);
145
+ /** Latest template props for RCS fallback init — avoids effect deps on unstable `templateData` references. */
146
+ const rcsFallbackTemplateSourceRef = useRef({ isFullMode, templateDetails, templateData });
147
+ rcsFallbackTemplateSourceRef.current = { isFullMode, templateDetails, templateData };
148
+
149
+ const fetchTagSchemaIfNewQuery = (query) => {
150
+ const key = JSON.stringify(query);
151
+ if (lastTagSchemaQueryKeyRef.current === key) return;
152
+ lastTagSchemaQueryKeyRef.current = key;
153
+ globalActions.fetchSchemaForEntity(query);
154
+ };
155
+
156
+ const traiDltEnabled = useMemo(
157
+ () => isTraiDLTEnable(isFullMode, smsRegister),
158
+ [isFullMode, smsRegister],
159
+ );
160
+ /**
161
+ * VarSegment for RCS SMS fallback when:
162
+ * - `isTraiDLTEnable` passes (library + `smsRegister === 'DLT'`, or full-mode library), or
163
+ * - org has TRAI DLT (`hasTraiDltFeature`) — needed when slidebox used to force `isFullMode={false}`
164
+ * or `smsRegister` is missing so `isTraiDLTEnable` alone is false, or
165
+ * - RCS template inline edit (`isRcsEditFlow`).
166
+ */
167
+ const useRcsFallbackVarSegment = useMemo(() => {
168
+ if (!isRcsSmsFallback) return false;
169
+ return traiDltEnabled || hasTraiDltFeature() || isRcsEditFlow;
170
+ }, [isRcsSmsFallback, traiDltEnabled, isRcsEditFlow]);
171
+
172
+ /**
173
+ * RCS SMS fallback: always show character count vs TRAI max (`SMS_TRAI_CONTENT_MAX_LENGTH`).
174
+ * Do not use `totalCharacters` ({smsCount} SMS via length/160) here: resolved template + variable
175
+ * values can exceed one GSM segment while DLT still shows a single registered template — length/160
176
+ * is only a rough segment hint and reads as “wrong SMS count” in campaigns.
177
+ */
178
+ const renderDescriptionCharacterCount = (className = "rcs-character-count") => (
179
+ <CapLabel type="label1" className={className}>
180
+ {formatMessage(messages.charactersCountLabel, {
181
+ current: totalMessageLength,
182
+ max: SMS_TRAI_CONTENT_MAX_LENGTH,
183
+ })}
184
+ </CapLabel>
185
+ );
186
+
187
+ const dltConsecutiveRunLength = (segments, index) => {
188
+ if (!Array.isArray(segments) || typeof segments[index] !== 'string' || !isDltHashVarToken(segments[index])) {
189
+ return 0;
190
+ }
191
+ let lo = index;
192
+ while (lo > 0 && isDltHashVarToken(segments[lo - 1])) lo -= 1;
193
+ let hi = index;
194
+ while (hi < segments.length - 1 && isDltHashVarToken(segments[hi + 1])) hi += 1;
195
+ return hi - lo + 1;
196
+ };
197
+
198
+ const renderRcsFallbackMessage = (str = '') => {
199
+ if (!useRcsFallbackVarSegment) {
200
+ return (
201
+ <CapRow className="rcs-create-template-message-input">
202
+ <div className="rcs_text_area_wrapper">
203
+ <TextArea
204
+ id="rcs_fallback_message_textarea"
205
+ autosize={{ minRows: 4, maxRows: 12 }}
206
+ value={fallbackText}
207
+ onChange={(e) => setFallbackText(e.target.value)}
208
+ placeholder={formatMessage(rcsMessages.fallbackMsgPlaceholder)}
209
+ data-testid="rcs_fallback_plain_text_area"
210
+ />
211
+ {renderDescriptionCharacterCount()}
212
+ </div>
213
+ </CapRow>
214
+ );
215
+ }
216
+ return (
217
+ <CapRow className="rcs-create-template-message-input">
218
+ <div className="rcs_text_area_wrapper">
219
+ <VarSegmentMessageEditor
220
+ templateString={str}
221
+ valueMap={fallbackVarMappedData || {}}
222
+ onChange={(varSegmentFieldId, nextSlotValue) => {
223
+ setFallbackVarMappedData((previousSlotMap) => ({
224
+ ...(previousSlotMap || {}),
225
+ [varSegmentFieldId]: nextSlotValue,
226
+ }));
227
+ }}
228
+ onFocus={setFallbackFocusedId}
229
+ varRegex={COMBINED_SMS_TEMPLATE_VAR_REGEX}
230
+ placeholderPrefix=""
231
+ getPlaceholder={() => formatMessage(rcsMessages.fallbackMsgPlaceholder)}
232
+ renderVarFooter={(dltSegmentToken, dltSegmentIndex) => {
233
+ if (!isDltHashVarToken(dltSegmentToken)) return null;
234
+ const segments = splitTemplateVarString(str, COMBINED_SMS_TEMPLATE_VAR_REGEX);
235
+ const varCounts = dltConsecutiveRunLength(segments, dltSegmentIndex);
236
+ if (varCounts < 1) return null;
237
+ /* Inline layout/color so hint is visible even if CapSpin / load order blocks SCSS */
238
+ const dltVarSlotHintStyle = {
239
+ display: 'block',
240
+ marginTop: CAP_SPACE_04,
241
+ marginLeft: 'auto',
242
+ marginRight: 0,
243
+ width: 'fit-content',
244
+ maxWidth: '100%',
245
+ textAlign: 'right',
246
+ color: 'rgba(0, 0, 0, 0.45)',
247
+ fontSize: 12,
248
+ fontWeight: 400,
249
+ lineHeight: '16px',
250
+ };
251
+ return (
252
+ <span className="sms-trai-rcs-fallback-var-hint" style={dltVarSlotHintStyle}>
253
+ {formatMessage(messages.textAreaCounts, {
254
+ varCounts,
255
+ var: SMS_TRAI_VAR,
256
+ charCounts: varCounts * CHARLIMIT,
257
+ })}
258
+ </span>
259
+ );
260
+ }}
261
+ />
262
+ {(!isFullMode)
263
+ ? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
264
+ : (isFullMode && renderDescriptionCharacterCount())}
265
+ </div>
266
+ </CapRow>
267
+ );
268
+ };
105
269
  const SMSTraiFooter = styled.div`
106
270
  background-color: ${CAP_WHITE};
107
271
  padding: ${CAP_SPACE_32} ${CAP_SPACE_24};
@@ -117,7 +281,6 @@ export const SmsTraiEdit = (props) => {
117
281
  .ant-btn {
118
282
  margin-right: ${CAP_SPACE_16};
119
283
  }
120
- }
121
284
  `;
122
285
  const TraiEditTemplateDetails = styled.div`
123
286
  margin-bottom: ${CAP_SPACE_16};
@@ -126,41 +289,127 @@ export const SmsTraiEdit = (props) => {
126
289
  }
127
290
  `;
128
291
 
292
+ /**
293
+ * RCS embedded SMS fallback receives a new `templateData` object from `mapFallbackValueToEditTemplateData`
294
+ * on every parent render. Depending on `[templateDetails || templateData]` re-ran init and wiped
295
+ * VarSegmentMessageEditor state whenever Test & Preview (or any) re-rendered Rcs. Key only changes
296
+ * when template id / sms-editor / header actually change.
297
+ *
298
+ * Do NOT include `unicode-validity` in the key: parent merges `onRcsFallbackEditorStateChange`
299
+ * unicode patches into `templateData`, which would change the key every toggle and re-run init —
300
+ * resetting local checkbox state (visible as the unicode control flipping).
301
+ *
302
+ * Do NOT include `rcs-sms-fallback-var-mapped` in the key: the parent merges slot edits into
303
+ * `templateData` on every change, so the key would change each keystroke and this effect would
304
+ * re-run and reset slots — visible as toggling between old and new values.
305
+ */
306
+ const rcsFallbackTemplateInitKey = useMemo(() => {
307
+ if (!isRcsSmsFallback) return null;
308
+ // RCS fallback slidebox always passes `templateData` from `mapFallbackValueToEditTemplateData`.
309
+ // `templateDetails` stays empty there (no route id fetch) — do not branch on `isFullMode` or init
310
+ // key is null, the effect never runs, and `loading` stays true forever.
311
+ const activeSmsTemplateSource = templateData;
312
+ if (!activeSmsTemplateSource || isEmpty(activeSmsTemplateSource)) return null;
313
+ const templateBase = get(activeSmsTemplateSource, 'versions.base', {}) || {};
314
+ const smsEditorTemplateString = templateBase?.['sms-editor'] ?? '';
315
+ const registeredSenderHeaderList = templateBase?.header;
316
+ const headerListFingerprint = Array.isArray(registeredSenderHeaderList)
317
+ ? registeredSenderHeaderList.join('\u001f')
318
+ : '';
319
+ const templateRecordId = activeSmsTemplateSource?._id ?? '';
320
+ return [
321
+ templateRecordId,
322
+ smsEditorTemplateString,
323
+ headerListFingerprint,
324
+ ].join('\u0000');
325
+ }, [isRcsSmsFallback, templateData]);
326
+
129
327
  useEffect(() => {
130
- //fetching tags
131
328
  const { type, module } = location.query || {};
132
329
  const isEmbedded = type === EMBEDDED;
133
330
  const query = {
134
331
  layout: SMS,
135
332
  type: TAG,
136
333
  context: isEmbedded ? module : DEFAULT,
137
- embedded: isEmbedded ? type : FULL,
334
+ embedded: forceFullTagContext ? FULL : (isEmbedded ? type : FULL),
138
335
  };
139
336
  if (getDefaultTags) {
140
337
  query.context = getDefaultTags;
141
338
  }
142
- globalActions.fetchSchemaForEntity(query);
143
- //fetching template data in fullmode
339
+ fetchTagSchemaIfNewQuery(query);
144
340
  const { id } = params || {};
145
341
  if (id) {
146
342
  actions.getTemplateDetails(id);
147
343
  }
148
- //cleanup code
149
344
  return () => {
150
345
  actions.resetEditTemplate();
151
- varMap = {};
346
+ varMapRef.current = {};
152
347
  };
153
348
  }, []);
154
349
 
155
- //computing placeholder array for mapping values and rendering dynamic form
156
350
  useEffect(() => {
157
- traiData = isFullMode ? templateDetails : templateData;
158
- if (traiData && !isEmpty(traiData)) {
159
- let msg = get(traiData, `versions.base.sms-editor`, '');
351
+ if (!isRcsSmsFallback || rcsFallbackTemplateInitKey == null) return;
352
+ const {
353
+ isFullMode: isFullModeFromRef,
354
+ templateDetails: templateDetailsFromRef,
355
+ templateData: templateDataFromRef,
356
+ } = rcsFallbackTemplateSourceRef.current;
357
+ const activeTemplateSourceForInit = isRcsSmsFallback
358
+ ? templateDataFromRef
359
+ : (isFullModeFromRef ? templateDetailsFromRef : templateDataFromRef);
360
+ if (!activeTemplateSourceForInit || isEmpty(activeTemplateSourceForInit)) return;
361
+ traiDataRef.current = activeTemplateSourceForInit;
362
+ const templateBase = get(activeTemplateSourceForInit, 'versions.base', {});
363
+ const unicodeValidity = get(templateBase, 'unicode-validity', true);
364
+ const templateMsg = get(activeTemplateSourceForInit, 'versions.base.sms-editor', '') || '';
365
+ if (!useRcsFallbackVarSegment) {
366
+ setFallbackText(templateMsg);
367
+ setFallbackVarMappedData({});
368
+ setUpdatedSmsEditor(String(templateMsg).split(''));
369
+ setTotalMessageLength(String(templateMsg).length);
370
+ updateIsUnicodeAllowed(unicodeValidity);
371
+ updateLoading(false);
372
+ return;
373
+ }
374
+ const savedVarMap = get(templateBase, 'rcs-sms-fallback-var-mapped', {}) || {};
375
+ const initialVarMap = {};
376
+ const templateSegments = splitTemplateVarString(templateMsg, COMBINED_SMS_TEMPLATE_VAR_REGEX);
377
+ let varOrdinal = 0;
378
+ templateSegments.forEach((segmentToken, segmentIndexInTemplate) => {
379
+ const isVar =
380
+ typeof segmentToken === 'string' && isAnyTemplateVarToken(segmentToken);
381
+ if (!isVar) return;
382
+ varOrdinal += 1;
383
+ const varSegmentSlotId = `${segmentToken}_${segmentIndexInTemplate}`;
384
+ const persistedSlotValue =
385
+ savedVarMap?.[varSegmentSlotId]
386
+ ?? savedVarMap?.[`${segmentToken}_${varOrdinal}`];
387
+ // Persisted '' means the user cleared the slot — must not fall back to `segmentToken` for mustache
388
+ // (that would resurrect {{…}} in the input and look like the tag "came back").
389
+ if (typeof persistedSlotValue === 'string') {
390
+ initialVarMap[varSegmentSlotId] = persistedSlotValue;
391
+ } else if (isDltHashVarToken(segmentToken)) {
392
+ initialVarMap[varSegmentSlotId] = '';
393
+ } else {
394
+ initialVarMap[varSegmentSlotId] = segmentToken;
395
+ }
396
+ });
397
+ setFallbackText(templateMsg);
398
+ setFallbackVarMappedData(initialVarMap);
399
+ const initialResolvedFallbackDisplay = getFallbackResolvedContent(templateMsg, initialVarMap);
400
+ setUpdatedSmsEditor(initialResolvedFallbackDisplay.split(''));
401
+ setTotalMessageLength(initialResolvedFallbackDisplay.length);
402
+ updateIsUnicodeAllowed(unicodeValidity);
403
+ updateLoading(false);
404
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- init only when semantic key changes; template props are read from ref
405
+ }, [isRcsSmsFallback, rcsFallbackTemplateInitKey, useRcsFallbackVarSegment]);
406
+
407
+ useEffect(() => {
408
+ if (isRcsSmsFallback) return;
409
+ traiDataRef.current = isFullMode ? templateDetails : templateData;
410
+ if (traiDataRef.current && !isEmpty(traiDataRef.current)) {
411
+ let msg = get(traiDataRef.current, `versions.base.sms-editor`, '');
160
412
  const templateMessageArray = [];
161
- //converting sms-editor string to an array split at '{#var#}'
162
- //split and push string before '{#var#}[0 to index]', push '{#var#}',
163
- //split and repeat for remaining string[index+7 to length]
164
413
  while (msg.length !== 0) {
165
414
  const index = msg.search(SMS_TRAI_VAR);
166
415
  if (index !== -1) {
@@ -174,30 +423,59 @@ export const SmsTraiEdit = (props) => {
174
423
  }
175
424
  const filteredTemplateMessageArray = templateMessageArray.filter((i) => i === 0 || i);
176
425
  updateTempMsgArray(filteredTemplateMessageArray);
177
- //stop spinner
178
426
  updateLoading(false);
179
427
  }
180
- }, [templateDetails || templateData]);
428
+ }, [templateDetails || templateData, isRcsSmsFallback, isFullMode]);
181
429
 
182
- //compute/get varMapped and updated-sms-editor
183
430
  useEffect(() => {
431
+ if (!isRcsSmsFallback) return;
432
+ if (!useRcsFallbackVarSegment) {
433
+ const plainFallbackSmsText = fallbackText || '';
434
+ setUpdatedSmsEditor(plainFallbackSmsText.split(''));
435
+ setTotalMessageLength(plainFallbackSmsText.length);
436
+ return;
437
+ }
438
+ const resolvedFallbackDisplay = getFallbackResolvedContent(
439
+ fallbackText || '',
440
+ fallbackVarMappedData || {},
441
+ );
442
+ setUpdatedSmsEditor(resolvedFallbackDisplay.split(''));
443
+ setTotalMessageLength(resolvedFallbackDisplay.length);
444
+ }, [isRcsSmsFallback, useRcsFallbackVarSegment, fallbackText, fallbackVarMappedData]);
445
+
446
+ useEffect(() => {
447
+ if (!isRcsSmsFallback) return;
448
+ if (typeof onRcsFallbackEditorStateChange !== 'function') return;
449
+ onRcsFallbackEditorStateChange({
450
+ rcsSmsFallbackVarMapped: fallbackVarMappedData || {},
451
+ });
452
+ }, [isRcsSmsFallback, fallbackVarMappedData, onRcsFallbackEditorStateChange]);
453
+
454
+ useEffect(() => {
455
+ if (!isRcsSmsFallback) return;
456
+ if (typeof onRcsFallbackEditorStateChange !== 'function') return;
457
+ onRcsFallbackEditorStateChange({
458
+ unicodeValidity: isUnicodeAllowed,
459
+ });
460
+ }, [isRcsSmsFallback, isUnicodeAllowed, onRcsFallbackEditorStateChange]);
461
+
462
+ useEffect(() => {
463
+ if (isRcsSmsFallback) return;
184
464
  if (tempMsgArray.length !== 0) {
185
- const traiBase = traiData?.versions?.base || {};
465
+ const traiBase = traiDataRef.current?.versions?.base || {};
186
466
  const {
187
467
  'var-mapped': varMapped = {},
188
468
  'updated-sms-editor': traiSmsEditor = '',
189
469
  'unicode-validity': unicodeValidity = true,
190
470
  } = traiBase;
191
- //if varMap and updated-sms-editor is already present on non first edits,use those values
192
471
  if (!isEmpty(varMapped)) {
193
- varMap = cloneDeep(varMapped);
472
+ varMapRef.current = cloneDeep(varMapped);
194
473
  if (isFullMode) {
195
474
  setUpdatedSmsEditor(traiSmsEditor);
196
475
  } else {
197
476
  computeUpdatedSmsEditor();
198
477
  }
199
478
  } else {
200
- //computing and setting varMap for first edit
201
479
  let varCount = 1;
202
480
  let horizontalSpaceCount = 0;
203
481
  for (let i = 0; i < tempMsgArray.length; i += 1) {
@@ -208,7 +486,7 @@ export const SmsTraiEdit = (props) => {
208
486
  horizontalSpaceCount += 1;
209
487
  }
210
488
  if (tempMsgArray[i] !== nextElem && nextElem?.replace(/[^\S\r\n]/gm, '') !== '') {
211
- varMap[`${tempMsgArray[i]}_${(i - varCount - horizontalSpaceCount) + 1}`] = {
489
+ varMapRef.current[`${tempMsgArray[i]}_${(i - varCount - horizontalSpaceCount) + 1}`] = {
212
490
  data: '',
213
491
  count: varCount,
214
492
  };
@@ -222,7 +500,6 @@ export const SmsTraiEdit = (props) => {
222
500
  setUpdatedSmsEditor(tempMsgArray);
223
501
  }
224
502
  updateIsUnicodeAllowed(unicodeValidity);
225
- // calcaulate message length here
226
503
  calculateTotalMessageLength();
227
504
  }
228
505
  }, [tempMsgArray]);
@@ -236,17 +513,84 @@ export const SmsTraiEdit = (props) => {
236
513
  }
237
514
  }, []);
238
515
 
516
+ useEffect(() => {
517
+ const runValidateTags = (content) =>
518
+ validateTags({
519
+ content,
520
+ tagsParam: tags,
521
+ location,
522
+ tagModule: getDefaultTags,
523
+ isFullMode,
524
+ }) || {};
525
+
526
+ if (isRcsSmsFallback) {
527
+ if (isFullMode) {
528
+ tagValidationResponseRef.current = {};
529
+ updateIsTagValidationError(false);
530
+ return;
531
+ }
532
+ // TRAI/DLT VarSegment: `validateIfTagClosed` only understands paired `{{…}}` and breaks on
533
+ // legitimate `{#…#}` / mixed TRAI shapes (extra `{`/`}` counts). Do not tie Done to it;
534
+ // slot completeness is enforced by `areAllRcsSmsFallbackVarSlotsFilled` on the RCS screen.
535
+ if (useRcsFallbackVarSegment) {
536
+ tagValidationResponseRef.current = {};
537
+ updateIsTagValidationError(false);
538
+ return;
539
+ }
540
+ const validationContent = fallbackText || '';
541
+ if (!validationContent.trim()) {
542
+ tagValidationResponseRef.current = {};
543
+ updateIsTagValidationError(false);
544
+ return;
545
+ }
546
+ tagValidationResponseRef.current = validateTags({
547
+ content: validationContent,
548
+ tagsParam: tags,
549
+ location,
550
+ tagModule: getDefaultTags,
551
+ isFullMode: true,
552
+ }) || {};
553
+ const braceErr = !!tagValidationResponseRef.current.isBraceError;
554
+ // Plain (non-VarSegment) fallback textarea: only unbalanced `{{…}}` disables Done.
555
+ updateIsTagValidationError(braceErr);
556
+ return;
557
+ }
558
+ if (
559
+ !isFullMode &&
560
+ updatedSmsEditor?.length > 0 &&
561
+ !updatedSmsEditor.includes(SMS_TRAI_VAR)
562
+ ) {
563
+ tagValidationResponseRef.current = runValidateTags(updatedSmsEditor.join(''));
564
+ const missing = (tagValidationResponseRef.current.missingTags || []).length > 0;
565
+ const braceErr = !!tagValidationResponseRef.current.isBraceError;
566
+ updateIsTagValidationError(missing || braceErr);
567
+ } else if (!isRcsSmsFallback) {
568
+ tagValidationResponseRef.current = {};
569
+ updateIsTagValidationError(false);
570
+ }
571
+ }, [
572
+ updatedSmsEditor,
573
+ tags,
574
+ isRcsSmsFallback,
575
+ isFullMode,
576
+ useRcsFallbackVarSegment,
577
+ fallbackText,
578
+ fallbackVarMappedData,
579
+ getDefaultTags,
580
+ location,
581
+ ]);
582
+
239
583
  const computeUpdatedSmsEditor = () => {
240
584
  const arr = [...tempMsgArray];
241
- const varMapKeys = Object.keys(varMap)?.map((key) => Number(key.slice(8)))?.sort((a, b) => a - b) || [];
242
- for (const key in varMap) {
243
- if (varMap[key].data !== '') {
585
+ const varMapKeys = Object.keys(varMapRef.current)?.map((key) => Number(key.slice(8)))?.sort((a, b) => a - b) || [];
586
+ for (const key in varMapRef.current) {
587
+ if (varMapRef.current[key].data !== '') {
244
588
  const _id = Number(key.slice(8)); //Eg: -> extracting index 1 from keys like {#var# } _1
245
589
  const loopIndex =
246
590
  varMapKeys[varMapKeys?.indexOf(_id) + 1] || arr.length;
247
591
  for (let i = _id; i < loopIndex; i += 1) {
248
592
  if (i === _id) {
249
- arr[i] = varMap[key].data; //data for first #var# of the textbox
593
+ arr[i] = varMapRef.current[key].data; //data for first #var# of the textbox
250
594
  } else if (arr[i] === SMS_TRAI_VAR) {
251
595
  arr[i] = ''; //'' for remaining #var# of a textbox
252
596
  }
@@ -256,8 +600,8 @@ export const SmsTraiEdit = (props) => {
256
600
  setUpdatedSmsEditor(arr);
257
601
  };
258
602
 
259
- //Saving on done start
260
603
  const onUpdateTemplateComplete = (editResponse, errorMsg) => {
604
+ updateLoading(false);
261
605
  if (editResponse?.templateId) {
262
606
  CapNotification.success({
263
607
  message: formatMessage(messages.smsEditNotification),
@@ -336,66 +680,121 @@ export const SmsTraiEdit = (props) => {
336
680
  };
337
681
 
338
682
  const onDoneCallback = () => {
339
- if (updatedSmsEditor.includes(SMS_TRAI_VAR) && !isFullMode) {
340
- //during save textbox should not be empty
683
+ // RCS fallback: allow Save when only "missing tags" ({{name}} may not be in TagList); still block unbalanced braces.
684
+ if (isTagValidationError) {
685
+ if (!isRcsSmsFallback) return;
686
+ const tagValidationSnapshot = tagValidationResponseRef.current ?? {};
687
+ if (tagValidationSnapshot.isBraceError) return;
688
+ }
689
+ const editorJoined = Array.isArray(updatedSmsEditor)
690
+ ? updatedSmsEditor.join('')
691
+ : String(updatedSmsEditor || '');
692
+ if (!isRcsSmsFallback && editorJoined.includes(SMS_TRAI_VAR) && !isFullMode) {
341
693
  updateIsValidationError(true);
342
694
  } else {
343
- //start spinner
344
- updateLoading(true);
345
- const traiVersions = traiData.versions || {};
346
- traiVersions.base = {
347
- ...traiVersions.base,
348
- 'var-mapped': varMap,
349
- 'updated-sms-editor': updatedSmsEditor,
350
- 'unicode-validity': isUnicodeAllowed,
351
- };
695
+ // Only show full-screen spinner when waiting on full-mode SMS edit API — not for RCS SMS fallback
696
+ // handoff (getFormSubscriptionData → SmsFallback closeSlidebox) and not for embedded sync handoff.
697
+ if (isFullMode && !isRcsSmsFallback) {
698
+ updateLoading(true);
699
+ }
700
+ if (!traiDataRef.current.versions) {
701
+ traiDataRef.current.versions = { base: {}, history: [] };
702
+ }
703
+ const traiVersions = traiDataRef.current.versions;
704
+ if (isRcsSmsFallback) {
705
+ const {
706
+ 'var-mapped': _ignoredVarMapped,
707
+ 'updated-sms-editor': _ignoredUpdatedSmsEditor,
708
+ 'rcs-sms-fallback-var-mapped': _ignoredFallbackVarMapped,
709
+ ...baseWithoutDerivedFields
710
+ } = traiVersions.base || {};
711
+ traiVersions.base = {
712
+ ...baseWithoutDerivedFields,
713
+ 'sms-editor': fallbackText || '',
714
+ 'unicode-validity': isUnicodeAllowed,
715
+ ...(useRcsFallbackVarSegment && {
716
+ 'rcs-sms-fallback-var-mapped': fallbackVarMappedData || {},
717
+ }),
718
+ };
719
+ } else {
720
+ traiVersions.base = {
721
+ ...traiVersions.base,
722
+ 'var-mapped': varMapRef.current,
723
+ 'updated-sms-editor': updatedSmsEditor,
724
+ 'unicode-validity': isUnicodeAllowed,
725
+ };
726
+ }
352
727
  traiVersions.history = [traiVersions.base];
353
- if (isFullMode) {
354
- actions.editTemplate(traiData, onUpdateTemplateComplete);
728
+ // RCS → SMS fallback (slidebox / inline): always hand off via getFormSubscriptionData so parent
729
+ // can persist fallback state and close the slide — never the standalone TRAI editTemplate API here.
730
+ if (isFullMode && !isRcsSmsFallback) {
731
+ actions.editTemplate(traiDataRef.current, onUpdateTemplateComplete);
355
732
  } else {
356
733
  getFormSubscriptionData({
357
- value: traiData.versions,
734
+ value: traiVersions,
735
+ // Consumers/tests read `versions.base` first (see getBaseFromSmsTraiFormData, rcsDltEditCompletionHandler).
736
+ versions: traiVersions,
358
737
  _id: params && params.id,
359
738
  validity: true,
360
739
  type: SMS,
361
740
  });
741
+ updateLoading(false);
362
742
  }
363
743
  }
364
744
  };
365
- //Saving on done end
366
745
 
367
- // tag code start
746
+ const locationQueryType = location?.query?.type;
747
+ const locationQueryModule = location?.query?.module;
748
+
368
749
  useEffect(() => {
369
- let tag =
370
- metaEntities && metaEntities.tags ? metaEntities.tags.standard : [];
371
- const { type, module } = location.query || {};
372
- if (type === EMBEDDED && module === LIBRARY && !getDefaultTags) {
750
+ let tag = getStandardTagsFromMeta(metaEntities);
751
+ if ((!Array.isArray(tag) || tag.length === 0) && Array.isArray(supportedTags) && supportedTags.length > 0) {
373
752
  tag = supportedTags;
374
753
  }
375
- updateTags(tag);
376
- }, [metaEntities]);
754
+ if (
755
+ locationQueryType === EMBEDDED &&
756
+ locationQueryModule === LIBRARY &&
757
+ !getDefaultTags
758
+ ) {
759
+ tag = supportedTags || [];
760
+ }
761
+ updateTags(Array.isArray(tag) ? tag : []);
762
+ }, [metaEntities, getDefaultTags, supportedTags, locationQueryType, locationQueryModule]);
377
763
 
378
764
  const handleOnTagsContextChange = (data) => {
765
+ // CapTagList passes "Outbound" | "Loyalty" from the module filter; TagList may pass "Outbound" on init.
766
+ if (data == null || data === '') return;
767
+ const normalizedContext = String(data).toLowerCase();
379
768
  const { type } = location.query || {};
380
769
  const isEmbedded = type === EMBEDDED;
381
770
  const query = {
382
771
  layout: SMS,
383
772
  type: TAG,
384
- context:
385
- (data || '').toLowerCase() === ALL
386
- ? DEFAULT
387
- : (data || '').toLowerCase(),
388
- embedded: isEmbedded ? type : FULL,
773
+ context: normalizedContext === ALL ? DEFAULT : normalizedContext,
774
+ embedded: forceFullTagContext ? FULL : (isEmbedded ? type : FULL),
389
775
  };
390
- globalActions.fetchSchemaForEntity(query);
776
+ fetchTagSchemaIfNewQuery(query);
391
777
  };
392
778
 
393
779
  const onTagSelect = (data) => {
394
- if (textAreaId >= 0 && varMap && updatedSmsEditor) {
780
+ if (isRcsSmsFallback) {
781
+ if (!useRcsFallbackVarSegment) {
782
+ setFallbackText((prev) => `${prev || ''}{{${data}}}`);
783
+ return;
784
+ }
785
+ if (!fallbackFocusedId) return;
786
+ const prevVal = fallbackVarMappedData?.[fallbackFocusedId] ?? '';
787
+ const nextVal = `${prevVal}{{${data}}}`;
788
+ setFallbackVarMappedData((prev) => ({
789
+ ...(prev || {}),
790
+ [fallbackFocusedId]: nextVal,
791
+ }));
792
+ return;
793
+ }
794
+ if (textAreaId >= 0 && varMapRef.current && updatedSmsEditor) {
395
795
  const arr = [...updatedSmsEditor];
396
- const varMapKeys = Object.keys(varMap)?.map((key) => Number(key.slice(8)))?.sort((a, b) => a - b) || [];
796
+ const varMapKeys = Object.keys(varMapRef.current)?.map((key) => Number(key.slice(8)))?.sort((a, b) => a - b) || [];
397
797
  const loopIndex = varMapKeys[varMapKeys?.indexOf(textAreaId) + 1] || arr.length;
398
- //when trying to insert tag in empty textarea,{#var#} is replaced with "" and then tag is added
399
798
  for (let i = textAreaId; i < loopIndex; i += 1) {
400
799
  if (arr[i] === SMS_TRAI_VAR) {
401
800
  arr[i] = '';
@@ -403,26 +802,23 @@ export const SmsTraiEdit = (props) => {
403
802
  }
404
803
  const messageData = `${arr[textAreaId]}{{${data}}}`;
405
804
  arr[textAreaId] = messageData;
406
- varMap[`${SMS_TRAI_VAR}_${textAreaId}`].data = messageData;
805
+ varMapRef.current[`${SMS_TRAI_VAR}_${textAreaId}`].data = messageData;
407
806
  setUpdatedSmsEditor(arr);
408
807
  }
409
808
  };
410
- //setting the id of currently selected text area, is used onTagSelect
809
+
411
810
  const setTextAreaId = (event) => {
412
811
  updateTextAreaId(Number(event.target.id));
413
812
  };
414
- // tag code end
415
813
 
416
- // on change event of Text Area
417
814
  const textAreaValueChange = ({ target: { value, id } }) => {
815
+ if (isRcsSmsFallback) return;
418
816
  const _id = Number(id);
419
817
  const arr = [...updatedSmsEditor];
420
- const varMapKeys = Object.keys(varMap)?.map((key) => Number(key.slice(8)))?.sort((a, b) => a - b) || [];
818
+ const varMapKeys = Object.keys(varMapRef.current)?.map((key) => Number(key.slice(8)))?.sort((a, b) => a - b) || [];
421
819
  const loopIndex = varMapKeys[varMapKeys?.indexOf(_id) + 1] || arr.length;
422
820
 
423
- //assign entered value to varMap
424
- varMap[`${SMS_TRAI_VAR}_${_id}`].data = value;
425
- //based on entered value update updatedSmsEditor
821
+ varMapRef.current[`${SMS_TRAI_VAR}_${_id}`].data = value;
426
822
  if (value === '') {
427
823
  for (let i = _id; i < loopIndex; i += 1) {
428
824
  if (i === _id || arr[i] === '') {
@@ -432,7 +828,7 @@ export const SmsTraiEdit = (props) => {
432
828
  } else {
433
829
  for (let i = _id; i < loopIndex; i += 1) {
434
830
  if (i === _id) {
435
- arr[i] = varMap[`${SMS_TRAI_VAR}_${_id}`].data;
831
+ arr[i] = varMapRef.current[`${SMS_TRAI_VAR}_${_id}`].data;
436
832
  } else if (arr[i] === SMS_TRAI_VAR) {
437
833
  arr[i] = '';
438
834
  }
@@ -535,18 +931,33 @@ export const SmsTraiEdit = (props) => {
535
931
  };
536
932
 
537
933
  const smsLengthForVar = () => (
538
- <CapHeading type="h5" style={{ marginTop: CAP_SPACE_04, float: 'right' }}>
539
- {formatMessage(messages.totalCharacters, {
540
- smsCount: Math.ceil(totalMessageLength / 160),
541
- number: totalMessageLength,
542
- })}
934
+ <CapHeading
935
+ type="h5"
936
+ className={isRcsSmsFallback ? 'rcs-character-count rcs-character-count--compact' : ''}
937
+ style={isRcsSmsFallback ? {} : { marginTop: CAP_SPACE_04, marginBottom: 0 }}
938
+ >
939
+ {isRcsSmsFallback
940
+ ? formatMessage(messages.charactersCountLabel, {
941
+ current: totalMessageLength,
942
+ max: SMS_TRAI_CONTENT_MAX_LENGTH,
943
+ })
944
+ : formatMessage(messages.totalCharacters, {
945
+ smsCount: Math.ceil(totalMessageLength / 160),
946
+ number: totalMessageLength,
947
+ })}
543
948
  </CapHeading>
544
949
  );
545
950
 
546
- // to compute the length of the message
547
- //40 characters is blocked per'{#var#}' if textbox is empty otherwise it will be textbox length
548
- // and the remaining string length is added to it
549
951
  const calculateTotalMessageLength = () => {
952
+ if (isRcsSmsFallback) {
953
+ if (useRcsFallbackVarSegment) {
954
+ const resolved = getFallbackResolvedContent(fallbackText || '', fallbackVarMappedData || {});
955
+ setTotalMessageLength(resolved.length);
956
+ } else {
957
+ setTotalMessageLength((fallbackText || '').length);
958
+ }
959
+ return;
960
+ }
550
961
  const msgLenWithoutVar = tempMsgArray
551
962
  ?.filter((i) => i !== SMS_TRAI_VAR)
552
963
  .join('');
@@ -557,22 +968,43 @@ export const SmsTraiEdit = (props) => {
557
968
 
558
969
  const calculateLenForTextBox = () => {
559
970
  let countVarChar = 0;
560
- Object.keys(varMap).forEach((i) => {
561
- if (varMap[i].data) {
562
- countVarChar += varMap[i].data?.length;
563
- if (!showMsgLengthNote && varMap[i].data?.length > varMap[i].count * CHARLIMIT) {
971
+ Object.keys(varMapRef.current).forEach((i) => {
972
+ if (varMapRef.current[i].data) {
973
+ countVarChar += varMapRef.current[i].data?.length;
974
+ if (!showMsgLengthNote && varMapRef.current[i].data?.length > varMapRef.current[i].count * CHARLIMIT) {
564
975
  updateshowMsgLengthNote(true);
565
976
  }
566
977
  } else {
567
- countVarChar += varMap[i].count * CHARLIMIT;
978
+ countVarChar += varMapRef.current[i].count * CHARLIMIT;
568
979
  }
569
980
  });
570
981
  return countVarChar;
571
982
  };
572
983
 
984
+ const tagValidationErrorMessage = () => {
985
+ const { missingTags = [], unsupportedTags = [], isBraceError } = tagValidationResponseRef.current || {};
986
+ const listForMessage = (unsupportedTags && unsupportedTags.length > 0)
987
+ ? unsupportedTags
988
+ : missingTags;
989
+ if (isBraceError) {
990
+ return <CapError>{formatMessage(globalMessages.unbalanacedCurlyBraces)}</CapError>;
991
+ }
992
+ if (listForMessage.length > 0) {
993
+ return (
994
+ <CapError>
995
+ {formatMessage(messages.unsupportedTagsValidationError, {
996
+ unsupportedTags: listForMessage,
997
+ })}
998
+ </CapError>
999
+ );
1000
+ }
1001
+ return null;
1002
+ };
1003
+
573
1004
  const disablehandler = () => {
574
- if (traiData && !isEmpty(traiData)) {
575
- const msg = get(traiData, `versions.base.sms-editor`, '');
1005
+ if (isRcsSmsFallback) return false;
1006
+ if (traiDataRef.current && !isEmpty(traiDataRef.current)) {
1007
+ const msg = get(traiDataRef.current, `versions.base.sms-editor`, '');
576
1008
  const index = msg.search(SMS_TRAI_VAR);
577
1009
  if (index === -1) {
578
1010
  return true;
@@ -586,7 +1018,6 @@ export const SmsTraiEdit = (props) => {
586
1018
  updateIsUnicodeAllowed(checked);
587
1019
  };
588
1020
 
589
- // Get template content for test and preview
590
1021
  const getTemplateContent = () => {
591
1022
  if (!updatedSmsEditor || updatedSmsEditor.length === 0) {
592
1023
  return '';
@@ -594,42 +1025,69 @@ export const SmsTraiEdit = (props) => {
594
1025
  return updatedSmsEditor.join('');
595
1026
  };
596
1027
 
597
- // Build formData for TestAndPreviewSlidebox - templateConfigs with templateId and template for DLT test
598
1028
  const getFormDataForTestAndPreview = () => {
599
- const smsBase = get(traiData, 'versions.base') || get(templateDetails, 'versions.base') || get(templateData, 'versions.base');
1029
+ const smsBase = get(traiDataRef.current, 'versions.base') || get(templateDetails, 'versions.base') || get(templateData, 'versions.base');
600
1030
  if (!smsBase || !smsBase.template_id) {
601
1031
  return {};
602
1032
  }
603
1033
  const templateRaw = smsBase['updated-sms-editor'] || smsBase['sms-editor'] || '';
604
1034
  const template = Array.isArray(templateRaw) ? templateRaw.join('') : templateRaw;
1035
+ const traiDlt = isTraiDLTEnable(isFullMode, smsRegister);
1036
+ const headerIds = get(traiDataRef.current, 'versions.base.header', []) || [];
605
1037
  return {
606
1038
  templateConfigs: {
607
- templateId: smsBase.template_id, template, traiDltEnabled: true, registeredSenderIds: get(traiData, `versions.base.header`, []),
1039
+ templateId: smsBase.template_id,
1040
+ template,
1041
+ traiDltEnabled: traiDlt,
1042
+ registeredSenderIds: traiDlt ? headerIds : [],
608
1043
  },
609
1044
  };
610
1045
  };
611
1046
 
612
- // Handle test and preview button click
613
1047
  const handleTestAndPreview = () => {
614
1048
  setShowTestAndPreviewSlidebox(true);
615
1049
  };
616
1050
 
617
- // Handle close test and preview slidebox
618
1051
  const handleCloseTestAndPreview = () => {
619
1052
  setShowTestAndPreviewSlidebox(false);
620
1053
  };
621
1054
 
1055
+ const shouldShowPreview =
1056
+ !isRcsSmsFallback || (showPreviewInRcsFallback && !hidePreview);
1057
+ const smsSidePreviewColumn = (
1058
+ <CapColumn span={8} offset={1}>
1059
+ <UnifiedPreview
1060
+ channel={SMS}
1061
+ content={updatedSmsEditor.join('')}
1062
+ device={ANDROID}
1063
+ showDeviceToggle={false}
1064
+ showHeader={false}
1065
+ formatMessage={formatMessage}
1066
+ senderId={isUnicodeAllowed ? 'Unicode' : 'ASCII'}
1067
+ />
1068
+ </CapColumn>
1069
+ );
1070
+
622
1071
  return (
623
1072
  <>
624
- <CapSpin spinning={loading || fetchingLiquidTags} tip={fetchingLiquidTags && formatMessage(formBuilderMessages.liquidSpinText)}>
1073
+ <CapSpin
1074
+ spinning={loading || fetchingLiquidTags}
1075
+ tip={fetchingLiquidTags && formatMessage(formBuilderMessages.liquidSpinText)}
1076
+ className={[
1077
+ isRcsSmsFallback && 'sms-trai-edit-rcs-fallback',
1078
+ isOverview && 'sms-trai-edit--overview',
1079
+ ]
1080
+ .filter(Boolean)
1081
+ .join(' ') || undefined}
1082
+ >
625
1083
  <CapRow>
626
- {traiData && !isEmpty(traiData) && (
1084
+ {traiDataRef.current && !isEmpty(traiDataRef.current) && !isRcsSmsFallback && (
627
1085
  <TraiEditTemplateDetails>
628
1086
  <CapLabelInline type="label1">
629
1087
  {formatMessage(messages.templateLabel)}
630
1088
  </CapLabelInline>
631
1089
  <CapLabelInline type="label2">
632
- {get(traiData, `versions.base.template_name`, '')}
1090
+ {get(traiDataRef.current, `versions.base.template_name`, '')}
633
1091
  </CapLabelInline>
634
1092
 
635
1093
  <CapLabelInline type="label1">
@@ -640,15 +1098,15 @@ export const SmsTraiEdit = (props) => {
640
1098
  {formatMessage(messages.senderIdlabel)}
641
1099
  </CapLabelInline>
642
1100
  <CapLabelInline type="label2">
643
- {[...get(traiData, `versions.base.header`, [])].join(', ')}
1101
+ {[...get(traiDataRef.current, `versions.base.header`, [])].join(', ')}
644
1102
  </CapLabelInline>
645
1103
  </TraiEditTemplateDetails>
646
1104
  )}
647
- <CapColumn span={14}>
1105
+ <CapColumn span={shouldShowPreview ? 14 : 24}>
648
1106
  <CapRow>
649
1107
  <CapHeader
650
1108
  title={formatMessage(messages.traiEditTitle)}
651
- size="regular"
1109
+ size={isRcsSmsFallback ? 'label1' : 'regular'}
652
1110
  suffix={(
653
1111
  <TagList
654
1112
  label={formatMessage(messages.addLabels)}
@@ -657,42 +1115,55 @@ export const SmsTraiEdit = (props) => {
657
1115
  tags={tags || []}
658
1116
  onContextChange={handleOnTagsContextChange}
659
1117
  injectedTags={injectedTags || {}}
660
- hidePopover={disablehandler()}
1118
+ channel={SMS}
1119
+ hidePopover={false}
1120
+ disabled={!isRcsSmsFallback && disablehandler()}
661
1121
  selectedOfferDetails={selectedOfferDetails}
662
1122
  eventContextTags={eventContextTags}
1123
+ popoverOverlayStyle={isRcsSmsFallback ? { zIndex: 10020 } : undefined}
1124
+ popoverOverlayClassName={isRcsSmsFallback ? 'sms-fallback-taglist-popover rcs-sms-fallback-taglist-popover' : undefined}
663
1125
  />
664
1126
  )}
665
1127
  />
666
1128
  </CapRow>
667
1129
 
668
- <CapRow
669
- style={{
670
- backgroundColor: CAP_G10,
671
- padding: CAP_SPACE_16,
672
- }}
1130
+ {isRcsSmsFallback ? (
1131
+ <>
1132
+ {renderRcsFallbackMessage(fallbackText)}
1133
+ </>
1134
+ ) : (
1135
+ <>
1136
+ <CapRow className="sms-trai-editor-segment-row">
1137
+ <div className="sms-trai-segmented-editor">
1138
+ {renderedContent()}
1139
+ </div>
1140
+ </CapRow>
1141
+ <CapRow className="sms-trai-length-row">
1142
+ {smsLengthForVar()}
1143
+ </CapRow>
1144
+ </>
1145
+ )}
1146
+ {isRcsSmsFallback && isTagValidationError && (
1147
+ <CapRow>
1148
+ {tagValidationErrorMessage()}
1149
+ </CapRow>
1150
+ )}
1151
+ {!isRcsSmsFallback && isTagValidationError && tagValidationErrorMessage()}
1152
+ <CapCheckbox
1153
+ onChange={unicodeHandler}
1154
+ checked={isUnicodeAllowed}
1155
+ disabled={
1156
+ isRcsSmsFallback
1157
+ ? isRcsEditFlow
1158
+ : disablehandler()
1159
+ }
673
1160
  >
674
- {renderedContent()}
675
- </CapRow>
676
- <CapRow>
677
- {smsLengthForVar()}
678
- </CapRow>
679
- <CapCheckbox onChange={unicodeHandler} checked={isUnicodeAllowed} disabled={disablehandler()}>
680
1161
  {formatMessage(messages.unicodeLabel)}
681
1162
  </CapCheckbox>
682
1163
  {showMsgLengthNote && <CapInfoNote message={<FormattedMessage {...messages.msgLengthNote} values={{ var: '{#var#}' }} />} />}
683
- <div style={{ marginBottom: '100px' }} />
684
- </CapColumn>
685
- <CapColumn span={8} offset={1}>
686
- <UnifiedPreview
687
- channel={SMS}
688
- content={updatedSmsEditor.join('')}
689
- device={ANDROID}
690
- showDeviceToggle={false}
691
- showHeader={false}
692
- formatMessage={formatMessage}
693
- senderId={isUnicodeAllowed ? 'Unicode' : 'ASCII'}
694
- />
1164
+ <div className="sms-trai-edit-bottom-spacer" />
695
1165
  </CapColumn>
1166
+ {shouldShowPreview && smsSidePreviewColumn}
696
1167
  </CapRow>
697
1168
  <SMSTraiFooter>
698
1169
  {isLiquidValidationError && !liquidErrorPanelDismissed && (
@@ -718,8 +1189,9 @@ export const SmsTraiEdit = (props) => {
718
1189
  <FormattedMessage {...messages.testAndPreviewButtonLabel} />
719
1190
  </CapButton>
720
1191
  <CapButton
721
- onClick={onSubmitWrapper}
1192
+ onClick={onDoneCallback}
722
1193
  className="create-msg"
1194
+ disabled={isTagValidationError}
723
1195
  >
724
1196
  <FormattedMessage {...messages.saveButtonLabel} />
725
1197
  </CapButton>
@@ -736,6 +1208,15 @@ export const SmsTraiEdit = (props) => {
736
1208
  );
737
1209
  };
738
1210
 
1211
+ SmsTraiEdit.propTypes = {
1212
+ isRcsSmsFallback: PropTypes.bool,
1213
+ isRcsEditFlow: PropTypes.bool,
1214
+ showPreviewInRcsFallback: PropTypes.bool,
1215
+ hidePreview: PropTypes.bool,
1216
+ isOverview: PropTypes.bool,
1217
+ onRcsFallbackEditorStateChange: PropTypes.func,
1218
+ };
1219
+
739
1220
  const mapStateToProps = createStructuredSelector({
740
1221
  templateDetails: makeSelectTemplateDetailsResponse(),
741
1222
  metaEntities: makeSelectMetaEntities(),