@capillarytech/creatives-library 9.0.14 → 9.0.15-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/constants/unified.js +32 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/common.js +8 -0
  6. package/utils/commonUtils.js +19 -1
  7. package/utils/rcsPayloadUtils.js +92 -0
  8. package/utils/templateVarUtils.js +201 -0
  9. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  10. package/utils/tests/templateVarUtils.test.js +204 -0
  11. package/v2Components/CapActionButton/constants.js +7 -0
  12. package/v2Components/CapActionButton/index.js +168 -109
  13. package/v2Components/CapActionButton/index.scss +156 -5
  14. package/v2Components/CapActionButton/messages.js +19 -3
  15. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  16. package/v2Components/CapImageUpload/index.js +2 -2
  17. package/v2Components/CapTagList/index.js +10 -0
  18. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  24. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +60 -11
  25. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +201 -23
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +355 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +761 -222
  32. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  35. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  36. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +284 -267
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  39. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  40. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  41. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  43. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  44. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  45. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  46. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/FormBuilder/Classic.js +4487 -0
  49. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +366 -0
  50. package/v2Components/FormBuilder/Functional/channels/registry.js +17 -0
  51. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +9 -0
  52. package/v2Components/FormBuilder/Functional/channels/sms/config.js +30 -0
  53. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +46 -0
  54. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +13 -0
  55. package/v2Components/FormBuilder/Functional/channels/sms/index.js +22 -0
  56. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +52 -0
  57. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +25 -0
  58. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +87 -0
  59. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +89 -0
  60. package/v2Components/FormBuilder/Functional/constants.js +42 -0
  61. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +38 -0
  62. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +85 -0
  63. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +81 -0
  64. package/v2Components/FormBuilder/Functional/core/store/selectors.js +30 -0
  65. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +91 -0
  66. package/v2Components/FormBuilder/Functional/index.js +26 -0
  67. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +59 -0
  68. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +31 -0
  69. package/v2Components/FormBuilder/Functional/layout/Section.js +116 -0
  70. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +258 -0
  71. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +21 -0
  72. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +65 -0
  73. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +97 -0
  74. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +192 -0
  75. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +129 -0
  76. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +132 -0
  77. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +40 -0
  78. package/v2Components/FormBuilder/Functional/tests/section.test.js +99 -0
  79. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +67 -0
  80. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +155 -0
  81. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +172 -0
  82. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +122 -0
  83. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +329 -0
  84. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +160 -0
  85. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +95 -0
  86. package/v2Components/FormBuilder/_formBuilder.scss +5 -0
  87. package/v2Components/FormBuilder/index.js +41 -4479
  88. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +114 -0
  89. package/v2Components/FormBuilder/tests/entryGate.test.js +106 -0
  90. package/v2Components/FormBuilder/tests/sms.characterization.test.js +336 -0
  91. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  92. package/v2Components/SmsFallback/constants.js +94 -0
  93. package/v2Components/SmsFallback/index.js +958 -0
  94. package/v2Components/SmsFallback/index.scss +266 -0
  95. package/v2Components/SmsFallback/messages.js +78 -0
  96. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  97. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  98. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  99. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  100. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  101. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  102. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  103. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  104. package/v2Components/TemplatePreview/_templatePreview.scss +41 -22
  105. package/v2Components/TemplatePreview/constants.js +2 -0
  106. package/v2Components/TemplatePreview/index.js +143 -31
  107. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  108. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  109. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  110. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  111. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  112. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  113. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  114. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  115. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  116. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  117. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  118. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  119. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  120. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  121. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  122. package/v2Containers/CreativesContainer/constants.js +11 -0
  123. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  124. package/v2Containers/CreativesContainer/index.js +323 -104
  125. package/v2Containers/CreativesContainer/index.scss +83 -1
  126. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  127. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  128. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  129. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  130. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +333 -90
  131. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  132. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  133. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  134. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  135. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  136. package/v2Containers/Rcs/constants.js +131 -11
  137. package/v2Containers/Rcs/index.js +2602 -807
  138. package/v2Containers/Rcs/index.scss +289 -8
  139. package/v2Containers/Rcs/messages.js +34 -3
  140. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +227 -0
  141. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +73535 -38537
  142. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -8
  143. package/v2Containers/Rcs/tests/index.test.js +147 -36
  144. package/v2Containers/Rcs/tests/mockData.js +38 -0
  145. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  146. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  147. package/v2Containers/Rcs/utils.js +478 -11
  148. package/v2Containers/Sms/Create/index.js +115 -48
  149. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  150. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  151. package/v2Containers/SmsTrai/Create/index.js +9 -4
  152. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  153. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  154. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  155. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  156. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  157. package/v2Containers/SmsWrapper/index.js +37 -8
  158. package/v2Containers/TagList/index.js +6 -0
  159. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  160. package/v2Containers/Templates/_templates.scss +171 -12
  161. package/v2Containers/Templates/actions.js +11 -0
  162. package/v2Containers/Templates/constants.js +2 -0
  163. package/v2Containers/Templates/index.js +125 -55
  164. package/v2Containers/Templates/sagas.js +57 -13
  165. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  166. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  167. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  168. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  169. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  170. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  171. package/v2Containers/TemplatesV2/index.js +86 -23
  172. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  173. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  174. package/v2Containers/Whatsapp/index.js +3 -20
  175. 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,
@@ -82,10 +100,20 @@ export const SmsTraiEdit = (props) => {
82
100
  templateData = {},
83
101
  selectedOfferDetails,
84
102
  eventContextTags,
85
- waitEventContextTags,
86
103
  fetchingLiquidTags,
87
104
  getLiquidTags,
88
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,
89
117
  } = props || {};
90
118
 
91
119
  const { formatMessage } = intl;
@@ -95,14 +123,149 @@ export const SmsTraiEdit = (props) => {
95
123
  const [tags, updateTags] = useState([]);
96
124
  const [textAreaId, updateTextAreaId] = useState();
97
125
  const [isValidationError, updateIsValidationError] = useState(false);
126
+ const [isTagValidationError, updateIsTagValidationError] = useState(false);
98
127
  const [totalMessageLength, setTotalMessageLength] = useState(0);
99
128
  const [isUnicodeAllowed, updateIsUnicodeAllowed] = useState(true);
129
+ const [fallbackText, setFallbackText] = useState('');
130
+ const [fallbackVarMappedData, setFallbackVarMappedData] = useState({});
131
+ const [fallbackFocusedId, setFallbackFocusedId] = useState('');
100
132
  const [showMsgLengthNote, updateshowMsgLengthNote] = useState(false);
101
133
  const [liquidErrorMessages, setLiquidErrorMessages] = useState({});
102
134
  const [isLiquidValidationError, setIsLiquidValidationError] = useState(false);
103
135
  /** After user closes the validation panel, keep it hidden until Save is clicked again (even if ErrorInfoNote remounts). */
104
136
  const [liquidErrorPanelDismissed, setLiquidErrorPanelDismissed] = useState(false);
105
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: '0.75rem',
248
+ fontWeight: 400,
249
+ lineHeight: '1rem',
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
+ };
106
269
  const SMSTraiFooter = styled.div`
107
270
  background-color: ${CAP_WHITE};
108
271
  padding: ${CAP_SPACE_32} ${CAP_SPACE_24};
@@ -118,7 +281,6 @@ export const SmsTraiEdit = (props) => {
118
281
  .ant-btn {
119
282
  margin-right: ${CAP_SPACE_16};
120
283
  }
121
- }
122
284
  `;
123
285
  const TraiEditTemplateDetails = styled.div`
124
286
  margin-bottom: ${CAP_SPACE_16};
@@ -127,41 +289,127 @@ export const SmsTraiEdit = (props) => {
127
289
  }
128
290
  `;
129
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
+
130
327
  useEffect(() => {
131
- //fetching tags
132
328
  const { type, module } = location.query || {};
133
329
  const isEmbedded = type === EMBEDDED;
134
330
  const query = {
135
331
  layout: SMS,
136
332
  type: TAG,
137
333
  context: isEmbedded ? module : DEFAULT,
138
- embedded: isEmbedded ? type : FULL,
334
+ embedded: forceFullTagContext ? FULL : (isEmbedded ? type : FULL),
139
335
  };
140
336
  if (getDefaultTags) {
141
337
  query.context = getDefaultTags;
142
338
  }
143
- globalActions.fetchSchemaForEntity(query);
144
- //fetching template data in fullmode
339
+ fetchTagSchemaIfNewQuery(query);
145
340
  const { id } = params || {};
146
341
  if (id) {
147
342
  actions.getTemplateDetails(id);
148
343
  }
149
- //cleanup code
150
344
  return () => {
151
345
  actions.resetEditTemplate();
152
- varMap = {};
346
+ varMapRef.current = {};
153
347
  };
154
348
  }, []);
155
349
 
156
- //computing placeholder array for mapping values and rendering dynamic form
157
350
  useEffect(() => {
158
- traiData = isFullMode ? templateDetails : templateData;
159
- if (traiData && !isEmpty(traiData)) {
160
- 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`, '');
161
412
  const templateMessageArray = [];
162
- //converting sms-editor string to an array split at '{#var#}'
163
- //split and push string before '{#var#}[0 to index]', push '{#var#}',
164
- //split and repeat for remaining string[index+7 to length]
165
413
  while (msg.length !== 0) {
166
414
  const index = msg.search(SMS_TRAI_VAR);
167
415
  if (index !== -1) {
@@ -175,30 +423,59 @@ export const SmsTraiEdit = (props) => {
175
423
  }
176
424
  const filteredTemplateMessageArray = templateMessageArray.filter((i) => i === 0 || i);
177
425
  updateTempMsgArray(filteredTemplateMessageArray);
178
- //stop spinner
179
426
  updateLoading(false);
180
427
  }
181
- }, [templateDetails || templateData]);
428
+ }, [templateDetails || templateData, isRcsSmsFallback, isFullMode]);
182
429
 
183
- //compute/get varMapped and updated-sms-editor
184
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;
185
464
  if (tempMsgArray.length !== 0) {
186
- const traiBase = traiData?.versions?.base || {};
465
+ const traiBase = traiDataRef.current?.versions?.base || {};
187
466
  const {
188
467
  'var-mapped': varMapped = {},
189
468
  'updated-sms-editor': traiSmsEditor = '',
190
469
  'unicode-validity': unicodeValidity = true,
191
470
  } = traiBase;
192
- //if varMap and updated-sms-editor is already present on non first edits,use those values
193
471
  if (!isEmpty(varMapped)) {
194
- varMap = cloneDeep(varMapped);
472
+ varMapRef.current = cloneDeep(varMapped);
195
473
  if (isFullMode) {
196
474
  setUpdatedSmsEditor(traiSmsEditor);
197
475
  } else {
198
476
  computeUpdatedSmsEditor();
199
477
  }
200
478
  } else {
201
- //computing and setting varMap for first edit
202
479
  let varCount = 1;
203
480
  let horizontalSpaceCount = 0;
204
481
  for (let i = 0; i < tempMsgArray.length; i += 1) {
@@ -209,7 +486,7 @@ export const SmsTraiEdit = (props) => {
209
486
  horizontalSpaceCount += 1;
210
487
  }
211
488
  if (tempMsgArray[i] !== nextElem && nextElem?.replace(/[^\S\r\n]/gm, '') !== '') {
212
- varMap[`${tempMsgArray[i]}_${(i - varCount - horizontalSpaceCount) + 1}`] = {
489
+ varMapRef.current[`${tempMsgArray[i]}_${(i - varCount - horizontalSpaceCount) + 1}`] = {
213
490
  data: '',
214
491
  count: varCount,
215
492
  };
@@ -223,7 +500,6 @@ export const SmsTraiEdit = (props) => {
223
500
  setUpdatedSmsEditor(tempMsgArray);
224
501
  }
225
502
  updateIsUnicodeAllowed(unicodeValidity);
226
- // calcaulate message length here
227
503
  calculateTotalMessageLength();
228
504
  }
229
505
  }, [tempMsgArray]);
@@ -237,17 +513,84 @@ export const SmsTraiEdit = (props) => {
237
513
  }
238
514
  }, []);
239
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
+
240
583
  const computeUpdatedSmsEditor = () => {
241
584
  const arr = [...tempMsgArray];
242
- const varMapKeys = Object.keys(varMap)?.map((key) => Number(key.slice(8)))?.sort((a, b) => a - b) || [];
243
- for (const key in varMap) {
244
- 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 !== '') {
245
588
  const _id = Number(key.slice(8)); //Eg: -> extracting index 1 from keys like {#var# } _1
246
589
  const loopIndex =
247
590
  varMapKeys[varMapKeys?.indexOf(_id) + 1] || arr.length;
248
591
  for (let i = _id; i < loopIndex; i += 1) {
249
592
  if (i === _id) {
250
- 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
251
594
  } else if (arr[i] === SMS_TRAI_VAR) {
252
595
  arr[i] = ''; //'' for remaining #var# of a textbox
253
596
  }
@@ -257,8 +600,8 @@ export const SmsTraiEdit = (props) => {
257
600
  setUpdatedSmsEditor(arr);
258
601
  };
259
602
 
260
- //Saving on done start
261
603
  const onUpdateTemplateComplete = (editResponse, errorMsg) => {
604
+ updateLoading(false);
262
605
  if (editResponse?.templateId) {
263
606
  CapNotification.success({
264
607
  message: formatMessage(messages.smsEditNotification),
@@ -337,66 +680,121 @@ export const SmsTraiEdit = (props) => {
337
680
  };
338
681
 
339
682
  const onDoneCallback = () => {
340
- if (updatedSmsEditor.includes(SMS_TRAI_VAR) && !isFullMode) {
341
- //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) {
342
693
  updateIsValidationError(true);
343
694
  } else {
344
- //start spinner
345
- updateLoading(true);
346
- const traiVersions = traiData.versions || {};
347
- traiVersions.base = {
348
- ...traiVersions.base,
349
- 'var-mapped': varMap,
350
- 'updated-sms-editor': updatedSmsEditor,
351
- 'unicode-validity': isUnicodeAllowed,
352
- };
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
+ }
353
727
  traiVersions.history = [traiVersions.base];
354
- if (isFullMode) {
355
- 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);
356
732
  } else {
357
733
  getFormSubscriptionData({
358
- value: traiData.versions,
734
+ value: traiVersions,
735
+ // Consumers/tests read `versions.base` first (see getBaseFromSmsTraiFormData, rcsDltEditCompletionHandler).
736
+ versions: traiVersions,
359
737
  _id: params && params.id,
360
738
  validity: true,
361
739
  type: SMS,
362
740
  });
741
+ updateLoading(false);
363
742
  }
364
743
  }
365
744
  };
366
- //Saving on done end
367
745
 
368
- // tag code start
746
+ const locationQueryType = location?.query?.type;
747
+ const locationQueryModule = location?.query?.module;
748
+
369
749
  useEffect(() => {
370
- let tag =
371
- metaEntities && metaEntities.tags ? metaEntities.tags.standard : [];
372
- const { type, module } = location.query || {};
373
- 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) {
374
752
  tag = supportedTags;
375
753
  }
376
- updateTags(tag);
377
- }, [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]);
378
763
 
379
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();
380
768
  const { type } = location.query || {};
381
769
  const isEmbedded = type === EMBEDDED;
382
770
  const query = {
383
771
  layout: SMS,
384
772
  type: TAG,
385
- context:
386
- (data || '').toLowerCase() === ALL
387
- ? DEFAULT
388
- : (data || '').toLowerCase(),
389
- embedded: isEmbedded ? type : FULL,
773
+ context: normalizedContext === ALL ? DEFAULT : normalizedContext,
774
+ embedded: forceFullTagContext ? FULL : (isEmbedded ? type : FULL),
390
775
  };
391
- globalActions.fetchSchemaForEntity(query);
776
+ fetchTagSchemaIfNewQuery(query);
392
777
  };
393
778
 
394
779
  const onTagSelect = (data) => {
395
- 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) {
396
795
  const arr = [...updatedSmsEditor];
397
- 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) || [];
398
797
  const loopIndex = varMapKeys[varMapKeys?.indexOf(textAreaId) + 1] || arr.length;
399
- //when trying to insert tag in empty textarea,{#var#} is replaced with "" and then tag is added
400
798
  for (let i = textAreaId; i < loopIndex; i += 1) {
401
799
  if (arr[i] === SMS_TRAI_VAR) {
402
800
  arr[i] = '';
@@ -404,26 +802,23 @@ export const SmsTraiEdit = (props) => {
404
802
  }
405
803
  const messageData = `${arr[textAreaId]}{{${data}}}`;
406
804
  arr[textAreaId] = messageData;
407
- varMap[`${SMS_TRAI_VAR}_${textAreaId}`].data = messageData;
805
+ varMapRef.current[`${SMS_TRAI_VAR}_${textAreaId}`].data = messageData;
408
806
  setUpdatedSmsEditor(arr);
409
807
  }
410
808
  };
411
- //setting the id of currently selected text area, is used onTagSelect
809
+
412
810
  const setTextAreaId = (event) => {
413
811
  updateTextAreaId(Number(event.target.id));
414
812
  };
415
- // tag code end
416
813
 
417
- // on change event of Text Area
418
814
  const textAreaValueChange = ({ target: { value, id } }) => {
815
+ if (isRcsSmsFallback) return;
419
816
  const _id = Number(id);
420
817
  const arr = [...updatedSmsEditor];
421
- 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) || [];
422
819
  const loopIndex = varMapKeys[varMapKeys?.indexOf(_id) + 1] || arr.length;
423
820
 
424
- //assign entered value to varMap
425
- varMap[`${SMS_TRAI_VAR}_${_id}`].data = value;
426
- //based on entered value update updatedSmsEditor
821
+ varMapRef.current[`${SMS_TRAI_VAR}_${_id}`].data = value;
427
822
  if (value === '') {
428
823
  for (let i = _id; i < loopIndex; i += 1) {
429
824
  if (i === _id || arr[i] === '') {
@@ -433,7 +828,7 @@ export const SmsTraiEdit = (props) => {
433
828
  } else {
434
829
  for (let i = _id; i < loopIndex; i += 1) {
435
830
  if (i === _id) {
436
- arr[i] = varMap[`${SMS_TRAI_VAR}_${_id}`].data;
831
+ arr[i] = varMapRef.current[`${SMS_TRAI_VAR}_${_id}`].data;
437
832
  } else if (arr[i] === SMS_TRAI_VAR) {
438
833
  arr[i] = '';
439
834
  }
@@ -536,18 +931,32 @@ export const SmsTraiEdit = (props) => {
536
931
  };
537
932
 
538
933
  const smsLengthForVar = () => (
539
- <CapHeading type="h5" style={{ marginTop: CAP_SPACE_04, float: 'right' }}>
540
- {formatMessage(messages.totalCharacters, {
541
- smsCount: Math.ceil(totalMessageLength / 160),
542
- number: totalMessageLength,
543
- })}
934
+ <CapHeading
935
+ type="h5"
936
+ className={isRcsSmsFallback ? 'rcs-character-count rcs-character-count--compact' : 'sms-trai-char-count-heading'}
937
+ >
938
+ {isRcsSmsFallback
939
+ ? formatMessage(messages.charactersCountLabel, {
940
+ current: totalMessageLength,
941
+ max: SMS_TRAI_CONTENT_MAX_LENGTH,
942
+ })
943
+ : formatMessage(messages.totalCharacters, {
944
+ smsCount: Math.ceil(totalMessageLength / 160),
945
+ number: totalMessageLength,
946
+ })}
544
947
  </CapHeading>
545
948
  );
546
949
 
547
- // to compute the length of the message
548
- //40 characters is blocked per'{#var#}' if textbox is empty otherwise it will be textbox length
549
- // and the remaining string length is added to it
550
950
  const calculateTotalMessageLength = () => {
951
+ if (isRcsSmsFallback) {
952
+ if (useRcsFallbackVarSegment) {
953
+ const resolved = getFallbackResolvedContent(fallbackText || '', fallbackVarMappedData || {});
954
+ setTotalMessageLength(resolved.length);
955
+ } else {
956
+ setTotalMessageLength((fallbackText || '').length);
957
+ }
958
+ return;
959
+ }
551
960
  const msgLenWithoutVar = tempMsgArray
552
961
  ?.filter((i) => i !== SMS_TRAI_VAR)
553
962
  .join('');
@@ -558,22 +967,43 @@ export const SmsTraiEdit = (props) => {
558
967
 
559
968
  const calculateLenForTextBox = () => {
560
969
  let countVarChar = 0;
561
- Object.keys(varMap).forEach((i) => {
562
- if (varMap[i].data) {
563
- countVarChar += varMap[i].data?.length;
564
- if (!showMsgLengthNote && varMap[i].data?.length > varMap[i].count * CHARLIMIT) {
970
+ Object.keys(varMapRef.current).forEach((i) => {
971
+ if (varMapRef.current[i].data) {
972
+ countVarChar += varMapRef.current[i].data?.length;
973
+ if (!showMsgLengthNote && varMapRef.current[i].data?.length > varMapRef.current[i].count * CHARLIMIT) {
565
974
  updateshowMsgLengthNote(true);
566
975
  }
567
976
  } else {
568
- countVarChar += varMap[i].count * CHARLIMIT;
977
+ countVarChar += varMapRef.current[i].count * CHARLIMIT;
569
978
  }
570
979
  });
571
980
  return countVarChar;
572
981
  };
573
982
 
983
+ const tagValidationErrorMessage = () => {
984
+ const { missingTags = [], unsupportedTags = [], isBraceError } = tagValidationResponseRef.current || {};
985
+ const listForMessage = (unsupportedTags && unsupportedTags.length > 0)
986
+ ? unsupportedTags
987
+ : missingTags;
988
+ if (isBraceError) {
989
+ return <CapError>{formatMessage(globalMessages.unbalanacedCurlyBraces)}</CapError>;
990
+ }
991
+ if (listForMessage.length > 0) {
992
+ return (
993
+ <CapError>
994
+ {formatMessage(messages.unsupportedTagsValidationError, {
995
+ unsupportedTags: listForMessage,
996
+ })}
997
+ </CapError>
998
+ );
999
+ }
1000
+ return null;
1001
+ };
1002
+
574
1003
  const disablehandler = () => {
575
- if (traiData && !isEmpty(traiData)) {
576
- const msg = get(traiData, `versions.base.sms-editor`, '');
1004
+ if (isRcsSmsFallback) return false;
1005
+ if (traiDataRef.current && !isEmpty(traiDataRef.current)) {
1006
+ const msg = get(traiDataRef.current, `versions.base.sms-editor`, '');
577
1007
  const index = msg.search(SMS_TRAI_VAR);
578
1008
  if (index === -1) {
579
1009
  return true;
@@ -587,7 +1017,6 @@ export const SmsTraiEdit = (props) => {
587
1017
  updateIsUnicodeAllowed(checked);
588
1018
  };
589
1019
 
590
- // Get template content for test and preview
591
1020
  const getTemplateContent = () => {
592
1021
  if (!updatedSmsEditor || updatedSmsEditor.length === 0) {
593
1022
  return '';
@@ -595,109 +1024,179 @@ export const SmsTraiEdit = (props) => {
595
1024
  return updatedSmsEditor.join('');
596
1025
  };
597
1026
 
598
- // Build formData for TestAndPreviewSlidebox - templateConfigs with templateId and template for DLT test
599
1027
  const getFormDataForTestAndPreview = () => {
600
- const smsBase = get(traiData, 'versions.base') || get(templateDetails, 'versions.base') || get(templateData, 'versions.base');
1028
+ const smsBase = get(traiDataRef.current, 'versions.base') || get(templateDetails, 'versions.base') || get(templateData, 'versions.base');
601
1029
  if (!smsBase || !smsBase.template_id) {
602
1030
  return {};
603
1031
  }
604
1032
  const templateRaw = smsBase['updated-sms-editor'] || smsBase['sms-editor'] || '';
605
1033
  const template = Array.isArray(templateRaw) ? templateRaw.join('') : templateRaw;
1034
+ const headerIds = get(traiDataRef.current, 'versions.base.header', []) || [];
1035
+ // For RCS SMS fallback, use the broader `useRcsFallbackVarSegment` flag (covers hasTraiDltFeature
1036
+ // and isRcsEditFlow cases where isTraiDLTEnable alone is false). For regular TRAI SMS edit,
1037
+ // use `traiDltEnabled` derived from isTraiDLTEnable — useRcsFallbackVarSegment is always false
1038
+ // when isRcsSmsFallback is false and would incorrectly disable DLT mode in Test & Preview.
1039
+ const effectiveDltEnabled = isRcsSmsFallback ? useRcsFallbackVarSegment : traiDltEnabled;
606
1040
  return {
607
1041
  templateConfigs: {
608
- templateId: smsBase.template_id, template, traiDltEnabled: true, registeredSenderIds: get(traiData, `versions.base.header`, []),
1042
+ templateId: smsBase.template_id,
1043
+ template,
1044
+ traiDltEnabled: effectiveDltEnabled,
1045
+ registeredSenderIds: effectiveDltEnabled ? headerIds : [],
609
1046
  },
610
1047
  };
611
1048
  };
612
1049
 
613
- // Handle test and preview button click
614
1050
  const handleTestAndPreview = () => {
615
1051
  setShowTestAndPreviewSlidebox(true);
616
1052
  };
617
1053
 
618
- // Handle close test and preview slidebox
619
1054
  const handleCloseTestAndPreview = () => {
620
1055
  setShowTestAndPreviewSlidebox(false);
621
1056
  };
622
1057
 
1058
+ const shouldShowPreview =
1059
+ !isRcsSmsFallback || (showPreviewInRcsFallback && !hidePreview);
1060
+ const smsSidePreviewColumn = (
1061
+ <CapColumn span={8} offset={0}>
1062
+ <UnifiedPreview
1063
+ channel={SMS}
1064
+ content={updatedSmsEditor.join('')}
1065
+ device={ANDROID}
1066
+ showDeviceToggle={false}
1067
+ showHeader={false}
1068
+ formatMessage={formatMessage}
1069
+ senderId={isUnicodeAllowed ? 'Unicode' : 'ASCII'}
1070
+ />
1071
+ </CapColumn>
1072
+ );
1073
+
623
1074
  return (
624
1075
  <>
625
- <CapSpin spinning={loading || fetchingLiquidTags} tip={fetchingLiquidTags && formatMessage(formBuilderMessages.liquidSpinText)}>
1076
+ <CapSpin
1077
+ spinning={loading || fetchingLiquidTags}
1078
+ tip={fetchingLiquidTags && formatMessage(formBuilderMessages.liquidSpinText)}
1079
+ className={[
1080
+ isRcsSmsFallback && 'sms-trai-edit-rcs-fallback',
1081
+ isOverview && 'sms-trai-edit--overview',
1082
+ ]
1083
+ .filter(Boolean)
1084
+ .join(' ') || undefined}
1085
+ >
626
1086
  <CapRow typeof='flex' align='top' noWrap fullWidth>
627
1087
  <CapRow type='flex' vertical fullWidth>
628
- {traiData && !isEmpty(traiData) && (
629
- <TraiEditTemplateDetails>
630
- <CapLabelInline type="label1">
631
- {formatMessage(messages.templateLabel)}
632
- </CapLabelInline>
633
- <CapLabelInline type="label2">
634
- {get(traiData, `versions.base.template_name`, '')}
635
- </CapLabelInline>
636
-
637
- <CapLabelInline type="label1">
638
- {formatMessage(messages.traiEditSeperator)}
639
- </CapLabelInline>
640
-
641
- <CapLabelInline type="label1">
642
- {formatMessage(messages.senderIdlabel)}
643
- </CapLabelInline>
644
- <CapLabelInline type="label2">
645
- {[...get(traiData, `versions.base.header`, [])].join(', ')}
646
- </CapLabelInline>
647
- </TraiEditTemplateDetails>
648
- )}
649
- <CapColumn span={24}>
650
- <CapRow useLegacy>
651
- <CapHeader
652
- title={formatMessage(messages.traiEditTitle)}
653
- size="regular"
654
- suffix={(
655
- <TagList
656
- label={formatMessage(messages.addLabels)}
657
- onTagSelect={onTagSelect}
658
- location={location}
659
- tags={tags || []}
660
- onContextChange={handleOnTagsContextChange}
661
- injectedTags={injectedTags || {}}
662
- hidePopover={disablehandler()}
663
- selectedOfferDetails={selectedOfferDetails}
664
- eventContextTags={eventContextTags}
665
- waitEventContextTags={waitEventContextTags}
666
- />
667
- )}
668
- />
669
- </CapRow>
1088
+ {traiDataRef.current && !isEmpty(traiDataRef.current) && !isRcsSmsFallback && (
1089
+ <TraiEditTemplateDetails>
1090
+ <CapLabelInline type="label1">
1091
+ {formatMessage(messages.templateLabel)}
1092
+ </CapLabelInline>
1093
+ <CapLabelInline type="label2">
1094
+ {get(traiDataRef.current, `versions.base.template_name`, '')}
1095
+ </CapLabelInline>
670
1096
 
671
- <CapRow
672
- useLegacy
673
- style={{
674
- backgroundColor: CAP_G10,
675
- padding: CAP_SPACE_16,
676
- }}
677
- >
678
- {renderedContent()}
679
- </CapRow>
1097
+ <CapLabelInline type="label1">
1098
+ {formatMessage(messages.traiEditSeperator)}
1099
+ </CapLabelInline>
1100
+
1101
+ <CapLabelInline type="label1">
1102
+ {formatMessage(messages.senderIdlabel)}
1103
+ </CapLabelInline>
1104
+ <CapLabelInline type="label2">
1105
+ {[...get(traiDataRef.current, `versions.base.header`, [])].join(', ')}
1106
+ </CapLabelInline>
1107
+ </TraiEditTemplateDetails>
1108
+ )}
1109
+ {isRcsSmsFallback &&
1110
+ traiDataRef.current &&
1111
+ !isEmpty(traiDataRef.current) &&
1112
+ !loading && (
1113
+ <TraiEditTemplateDetails className="sms-trai-edit-rcs-fallback__template-meta">
1114
+ <CapLabelInline type="label1">
1115
+ {formatMessage(messages.templateNameLabel)}
1116
+ </CapLabelInline>
1117
+ <CapLabelInline type="label2">
1118
+ {get(traiDataRef.current, 'versions.base.template_name', '')
1119
+ || get(traiDataRef.current, 'name', '')}
1120
+ </CapLabelInline>
1121
+ {traiDltEnabled ? (
1122
+ <>
1123
+ <CapLabelInline type="label1">
1124
+ {formatMessage(messages.traiEditSeperator)}
1125
+ </CapLabelInline>
1126
+ <CapLabelInline type="label1">
1127
+ {formatMessage(messages.senderIdlabel)}
1128
+ </CapLabelInline>
1129
+ <CapLabelInline type="label2">
1130
+ {[...get(traiDataRef.current, 'versions.base.header', [])].join(', ')}
1131
+ </CapLabelInline>
1132
+ </>
1133
+ ) : null}
1134
+ </TraiEditTemplateDetails>
1135
+ )}
1136
+ <CapColumn span={shouldShowPreview ? 16 : 24}>
1137
+ <CapRow useLegacy>
1138
+ <CapHeader
1139
+ title={formatMessage(messages.traiEditTitle)}
1140
+ size={isRcsSmsFallback ? 'label1' : 'regular'}
1141
+ suffix={(
1142
+ <TagList
1143
+ label={formatMessage(messages.addLabels)}
1144
+ onTagSelect={onTagSelect}
1145
+ location={location}
1146
+ tags={tags || []}
1147
+ onContextChange={handleOnTagsContextChange}
1148
+ injectedTags={injectedTags || {}}
1149
+ channel={SMS}
1150
+ hidePopover={false}
1151
+ disabled={!isRcsSmsFallback && disablehandler()}
1152
+ selectedOfferDetails={selectedOfferDetails}
1153
+ eventContextTags={eventContextTags}
1154
+ popoverOverlayStyle={isRcsSmsFallback ? { zIndex: 10020 } : undefined}
1155
+ popoverOverlayClassName={isRcsSmsFallback ? 'sms-fallback-taglist-popover rcs-sms-fallback-taglist-popover' : undefined}
1156
+ />
1157
+ )}
1158
+ />
1159
+ </CapRow>
1160
+
1161
+ {isRcsSmsFallback ? (
1162
+ <>
1163
+ {renderRcsFallbackMessage(fallbackText)}
1164
+ </>
1165
+ ) : (
1166
+ <>
1167
+ <CapRow useLegacy className="sms-trai-editor-segment-row">
1168
+ <div className="sms-trai-segmented-editor">
1169
+ {renderedContent()}
1170
+ </div>
1171
+ </CapRow>
1172
+ <CapRow useLegacy className="sms-trai-length-row">
1173
+ {smsLengthForVar()}
1174
+ </CapRow>
1175
+ </>
1176
+ )}
1177
+ {isRcsSmsFallback && isTagValidationError && (
680
1178
  <CapRow useLegacy>
681
- {smsLengthForVar()}
1179
+ {tagValidationErrorMessage()}
682
1180
  </CapRow>
683
- <CapCheckbox onChange={unicodeHandler} checked={isUnicodeAllowed} disabled={disablehandler()} style={{ marginTop: '1rem' }}>
684
- {formatMessage(messages.unicodeLabel)}
685
- </CapCheckbox>
686
- {showMsgLengthNote && <CapInfoNote message={<FormattedMessage {...messages.msgLengthNote} values={{ var: '{#var#}' }} />} />}
687
- <div style={{ marginBottom: '100px' }} />
688
- </CapColumn>
689
- </CapRow>
690
- <CapColumn span={8} offset={1}>
691
- <UnifiedPreview
692
- channel={SMS}
693
- content={updatedSmsEditor.join('')}
694
- device={ANDROID}
695
- showDeviceToggle={false}
696
- showHeader={false}
697
- formatMessage={formatMessage}
698
- senderId={isUnicodeAllowed ? 'Unicode' : 'ASCII'}
699
- />
1181
+ )}
1182
+ {!isRcsSmsFallback && isTagValidationError && tagValidationErrorMessage()}
1183
+ <CapCheckbox
1184
+ onChange={unicodeHandler}
1185
+ checked={isUnicodeAllowed}
1186
+ disabled={
1187
+ isRcsSmsFallback
1188
+ ? isRcsEditFlow
1189
+ : disablehandler()
1190
+ }
1191
+ style={{ marginTop: '1rem' }}
1192
+ >
1193
+ {formatMessage(messages.unicodeLabel)}
1194
+ </CapCheckbox>
1195
+ {showMsgLengthNote && <CapInfoNote message={<FormattedMessage {...messages.msgLengthNote} values={{ var: '{#var#}' }} />} />}
1196
+ <div className="sms-trai-edit-bottom-spacer" />
700
1197
  </CapColumn>
1198
+ </CapRow>
1199
+ {shouldShowPreview && smsSidePreviewColumn}
701
1200
  </CapRow>
702
1201
  <SMSTraiFooter>
703
1202
  {isLiquidValidationError && !liquidErrorPanelDismissed && (
@@ -723,8 +1222,9 @@ export const SmsTraiEdit = (props) => {
723
1222
  <FormattedMessage {...messages.testAndPreviewButtonLabel} />
724
1223
  </CapButton>
725
1224
  <CapButton
726
- onClick={onSubmitWrapper}
1225
+ onClick={onDoneCallback}
727
1226
  className="create-msg"
1227
+ disabled={isTagValidationError}
728
1228
  >
729
1229
  <FormattedMessage {...messages.saveButtonLabel} />
730
1230
  </CapButton>
@@ -741,6 +1241,15 @@ export const SmsTraiEdit = (props) => {
741
1241
  );
742
1242
  };
743
1243
 
1244
+ SmsTraiEdit.propTypes = {
1245
+ isRcsSmsFallback: PropTypes.bool,
1246
+ isRcsEditFlow: PropTypes.bool,
1247
+ showPreviewInRcsFallback: PropTypes.bool,
1248
+ hidePreview: PropTypes.bool,
1249
+ isOverview: PropTypes.bool,
1250
+ onRcsFallbackEditorStateChange: PropTypes.func,
1251
+ };
1252
+
744
1253
  const mapStateToProps = createStructuredSelector({
745
1254
  templateDetails: makeSelectTemplateDetailsResponse(),
746
1255
  metaEntities: makeSelectMetaEntities(),