@capillarytech/creatives-library 8.0.358 → 8.0.359-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 (125) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapImageUpload/index.js +2 -2
  15. package/v2Components/CapTagList/index.js +10 -0
  16. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  23. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -76
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +150 -4
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  29. package/v2Components/CommonTestAndPreview/index.js +810 -222
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  41. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  42. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  43. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  44. package/v2Components/FormBuilder/index.js +5 -4
  45. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  46. package/v2Components/SmsFallback/constants.js +73 -0
  47. package/v2Components/SmsFallback/index.js +956 -0
  48. package/v2Components/SmsFallback/index.scss +265 -0
  49. package/v2Components/SmsFallback/messages.js +78 -0
  50. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  51. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  52. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  56. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  57. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  58. package/v2Components/TemplatePreview/_templatePreview.scss +37 -22
  59. package/v2Components/TemplatePreview/constants.js +2 -0
  60. package/v2Components/TemplatePreview/index.js +143 -31
  61. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  62. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  63. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  64. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  65. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  66. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  67. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  68. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  69. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  70. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  71. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  72. package/v2Containers/CreativesContainer/constants.js +9 -0
  73. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  74. package/v2Containers/CreativesContainer/index.js +322 -103
  75. package/v2Containers/CreativesContainer/index.scss +83 -1
  76. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  77. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  78. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  81. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  82. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  83. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  84. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  85. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  86. package/v2Containers/Rcs/constants.js +120 -11
  87. package/v2Containers/Rcs/index.js +2577 -812
  88. package/v2Containers/Rcs/index.scss +281 -8
  89. package/v2Containers/Rcs/messages.js +34 -3
  90. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  91. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98036 -70145
  92. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  93. package/v2Containers/Rcs/tests/index.test.js +152 -121
  94. package/v2Containers/Rcs/tests/mockData.js +38 -0
  95. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  96. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  97. package/v2Containers/Rcs/utils.js +478 -11
  98. package/v2Containers/Sms/Create/index.js +106 -40
  99. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  100. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  101. package/v2Containers/SmsTrai/Create/index.js +9 -4
  102. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  103. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  104. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  105. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  106. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  107. package/v2Containers/SmsWrapper/index.js +37 -8
  108. package/v2Containers/TagList/index.js +6 -0
  109. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  110. package/v2Containers/Templates/_templates.scss +166 -9
  111. package/v2Containers/Templates/actions.js +11 -0
  112. package/v2Containers/Templates/constants.js +2 -0
  113. package/v2Containers/Templates/index.js +121 -53
  114. package/v2Containers/Templates/sagas.js +56 -12
  115. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  116. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  117. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  118. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  119. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  120. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  121. package/v2Containers/TemplatesV2/index.js +86 -23
  122. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  123. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  124. package/v2Containers/Whatsapp/index.js +3 -20
  125. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -44,10 +44,6 @@ class TemplateNameInputField extends React.Component {
44
44
  }
45
45
  }
46
46
  import PropTypes from 'prop-types';
47
- import {
48
- CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
49
- } from '@capillarytech/cap-ui-library/styled/variables';
50
-
51
47
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
52
48
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
53
49
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
@@ -57,12 +53,11 @@ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
57
53
  import { injectIntl, FormattedMessage } from 'react-intl';
58
54
  import classnames from 'classnames';
59
55
  import {
60
- isEmpty, get, forEach, cloneDeep, debounce,
56
+ isEmpty, get, forEach, cloneDeep, debounce, pick,
61
57
  } from 'lodash';
62
58
  import { connect } from 'react-redux';
63
59
  import { createStructuredSelector } from 'reselect';
64
60
  import { bindActionCreators, compose } from 'redux';
65
- import styled from 'styled-components';
66
61
  import { GA } from '@capillarytech/cap-ui-utils';
67
62
  import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
68
63
 
@@ -76,6 +71,7 @@ import SlideBoxContent from './SlideBoxContent';
76
71
  import * as constants from './constants';
77
72
  import * as commonUtil from '../../utils/common';
78
73
  import { gtmPush } from '../../utils/gtmTrackers';
74
+ import { normalizeRcsMessageContentForApi } from '../../utils/rcsPayloadUtils';
79
75
  import './index.scss';
80
76
  import * as templateActions from '../Templates/actions';
81
77
  import * as globalActions from '../Cap/actions';
@@ -91,6 +87,9 @@ import {
91
87
  import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants';
92
88
  import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
93
89
  import { RCS_STATUSES } from '../Rcs/constants';
90
+ import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
91
+ import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
92
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
94
93
  import { CREATIVE } from '../Facebook/constants';
95
94
  import { LOYALTY } from '../App/constants';
96
95
  import {
@@ -105,6 +104,11 @@ import { capSagaForFetchSchemaForEntity, capSagaLiquidEntity } from '../Cap/saga
105
104
  import { v2TemplateSagaWatchGetDefaultBeeTemplates } from '../Templates/sagas';
106
105
  import { DYNAMIC_URL } from '../../v2Components/CapWhatsappCTA/constants';
107
106
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
107
+ import SlideBoxWrapper from './CreativesSlideBoxWrapper';
108
+ import {
109
+ computeLiquidFooterUpdateFromFormBuilder,
110
+ getSlideBoxWrapperMarginFromLiquidErrors,
111
+ } from './embeddedSlideboxUtils';
108
112
 
109
113
  import {
110
114
  transformChannelPayload,
@@ -113,51 +117,24 @@ import {
113
117
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
114
118
  import { BIG_HTML } from '../InApp/constants';
115
119
 
116
- /**
117
- * Returns true if value is "deep empty": no errors present.
118
- * - null/undefined: empty
119
- * - string: empty if length === 0
120
- * - array: empty if length === 0
121
- * - plain object (e.g. { android: [], ios: [], generic: [] }): empty only if every value is deep-empty
122
- */
123
- function isDeepEmpty(value) {
124
- if (value == null) return true;
125
- if (typeof value === 'string') return value.length === 0;
126
- if (Array.isArray(value)) return value.length === 0;
127
- if (typeof value === 'object') {
128
- return Object.values(value).every(isDeepEmpty);
129
- }
130
- return false;
131
- }
132
-
133
120
  const classPrefix = 'add-creatives-section';
134
121
  const CREATIVES_CONTAINER = 'creativesContainer';
135
122
 
136
- const SlideBoxWrapper = styled.div`
137
- .cap-slide-box-v2-container{
138
- .slidebox-header, .slidebox-content-container{
139
- margin-bottom: ${({ slideBoxWrapperMargin }) => `${slideBoxWrapperMargin}`};
140
- padding: 0 rem;
141
- &.has-footer{
142
- overflow-x: hidden;
143
- }
144
- }
145
- .slidebox-footer{
146
- /* Only apply margin-bottom to footer when ErrorInfoNote is shown in footer (BEE editor) */
147
- /* For HTML Editor, errors are shown in ValidationErrorDisplay (inside content area), so no footer margin needed */
148
- margin-bottom: ${({ shouldApplyFooterMargin }) => (shouldApplyFooterMargin ? `${CAP_SPACE_16}` : '0')};
149
- padding: 0 rem;
150
- &.has-footer{
151
- overflow-x: hidden;
152
- }
153
- }
154
- }
155
- `;
156
123
  export class Creatives extends React.Component {
157
124
  constructor(props) {
158
125
  super(props);
159
126
 
160
- const initialSlidBoxContent = this.getSlideBoxContent({ mode: props.creativesMode, templateData: props.templateData, isFullMode: props.isFullMode });
127
+ const useLocalTemplates = get(
128
+ props,
129
+ 'localTemplatesConfig.useLocalTemplates',
130
+ get(props, 'useLocalTemplates', false),
131
+ );
132
+ const initialSlidBoxContent = this.getSlideBoxContent({
133
+ mode: props.creativesMode,
134
+ templateData: props.templateData,
135
+ isFullMode: props.isFullMode,
136
+ useLocalTemplates,
137
+ });
161
138
 
162
139
  this.state = {
163
140
  isLoadingContent: true,
@@ -204,7 +181,13 @@ export class Creatives extends React.Component {
204
181
  }
205
182
 
206
183
  componentWillUnmount() {
207
- if (get(this.props, 'location.query.type', '') === "embedded") {
184
+ const isEmbedded = get(this.props, 'location.query.type', '') === "embedded";
185
+ const useLocalTemplates = get(
186
+ this.props,
187
+ 'localTemplatesConfig.useLocalTemplates',
188
+ get(this.props, 'useLocalTemplates', false),
189
+ );
190
+ if (isEmbedded && !useLocalTemplates) {
208
191
  this.props.templateActions.resetTemplateStoreData();
209
192
  }
210
193
  this.props.globalActions.clearMetaEntities();
@@ -812,15 +795,69 @@ export class Creatives extends React.Component {
812
795
  smsFallBackContent = {},
813
796
  creativeName = "",
814
797
  channel = constants.RCS,
798
+ rcsCardVarMapped,
815
799
  accountId = "",
816
800
  } = templateData || {};
817
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
801
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
802
+ const isCarouselRcs = (rcsContent?.cardType || '').toString().toLowerCase() === 'carousel';
803
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
804
+ const {
805
+ cardDisplayTitle: _omitDispTitleIn,
806
+ cardDisplayDescription: _omitDispDescIn,
807
+ ...cardContent
808
+ } = firstCardIn;
818
809
  const Status = RCS_STATUSES.approved || '';
810
+ const mergedCardVarMapped = (() => {
811
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
812
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
813
+ const nestedRecord =
814
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
815
+ ? nestedCardVarMapped
816
+ : {};
817
+ const rootRecord =
818
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
819
+ ? rootMirrorCardVarMapped
820
+ : {};
821
+ const mergedFromRootAndNested = {
822
+ ...pickRcsCardVarMappedEntries(rootRecord),
823
+ ...pickRcsCardVarMappedEntries(nestedRecord),
824
+ };
825
+ return Object.keys(mergedFromRootAndNested).length > 0
826
+ ? mergedFromRootAndNested
827
+ : null;
828
+ })();
829
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
830
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
831
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
832
+ const includeRootRcsCardVarMapped =
833
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
834
+
835
+ const builtCardContent = isCarouselRcs
836
+ ? (rcsContent.cardContent || []).map((card, idx) => {
837
+ const {
838
+ cardDisplayTitle: _dt,
839
+ cardDisplayDescription: _dd,
840
+ ...restCard
841
+ } = card || {};
842
+ return {
843
+ ...restCard,
844
+ ...(idx === 0 && mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
845
+ Status,
846
+ };
847
+ })
848
+ : [
849
+ {
850
+ ...cardContent,
851
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
852
+ Status,
853
+ },
854
+ ];
819
855
 
820
856
  creativesTemplateData = {
821
857
  type: channel,
822
858
  edit: true,
823
859
  name: creativeName,
860
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
824
861
  versions: {
825
862
  base: {
826
863
  content: {
@@ -828,12 +865,7 @@ export class Creatives extends React.Component {
828
865
  rcsContent: {
829
866
  ...rcsContent,
830
867
  ...(accountId && !isFullMode && { accountId }),
831
- cardContent: [
832
- {
833
- ...cardContent,
834
- Status,
835
- },
836
- ],
868
+ cardContent: builtCardContent,
837
869
  },
838
870
  smsFallBackContent,
839
871
  },
@@ -981,7 +1013,10 @@ export class Creatives extends React.Component {
981
1013
  return newExpandableDetails;
982
1014
  }
983
1015
 
984
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
1016
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
1017
+ const channel = String(
1018
+ channelParam || template?.type || get(template, 'value.type') || '',
1019
+ ).toUpperCase();
985
1020
  let templateData = { channel };
986
1021
  switch (channel) {
987
1022
  case constants.SMS:
@@ -1328,28 +1363,104 @@ export class Creatives extends React.Component {
1328
1363
  break;
1329
1364
  case constants.RCS:
1330
1365
  if (template.value) {
1331
- const { name = "", versions = {} } = {
1332
- } = template.value || {};
1333
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1366
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1367
+ const { name = "", versions = {} } = template.value || {};
1368
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1369
+ const fromRecords = {
1370
+ ...(templateRecords?.smsFallBackContent || {}),
1371
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1372
+ };
1373
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1374
+ if (
1375
+ !smsFallbackPayload
1376
+ || typeof smsFallbackPayload !== 'object'
1377
+ || Object.keys(smsFallbackPayload).length === 0
1378
+ ) {
1379
+ return false;
1380
+ }
1381
+ const fallbackBodyText = String(
1382
+ smsFallbackPayload.smsContent
1383
+ ?? smsFallbackPayload.smsTemplateContent
1384
+ ?? smsFallbackPayload.message
1385
+ ?? smsFallbackPayload.content
1386
+ ?? '',
1387
+ ).trim();
1388
+ const fallbackTemplateName = String(
1389
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1390
+ ).trim();
1391
+ const rcsSmsFallbackVarMapped =
1392
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1393
+ const hasVarMappedEntries =
1394
+ rcsSmsFallbackVarMapped != null
1395
+ && typeof rcsSmsFallbackVarMapped === 'object'
1396
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1397
+ return (
1398
+ fallbackBodyText !== ''
1399
+ || fallbackTemplateName !== ''
1400
+ || hasVarMappedEntries
1401
+ );
1402
+ };
1403
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1404
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1405
+ ? { ...fromRecords, ...fromSubmit }
1406
+ : { ...fromSubmit, ...fromRecords };
1334
1407
  const {
1335
- cardContent = [],
1408
+ cardContent: cardContentFromSubmit = [],
1336
1409
  contentType = "",
1337
1410
  cardType = "",
1338
1411
  cardSettings = {},
1339
1412
  accountId = "",
1340
1413
  } = get(versions, 'base.content.RCS.rcsContent', {});
1414
+ const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1415
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1416
+ ? cardContentFromSubmit[0]
1417
+ : null;
1418
+ const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
1419
+ cardContentFromSubmit,
1420
+ rootRcsCardVarMappedFromSubmit,
1421
+ isFullModeForRcsConsumerPayload,
1422
+ );
1341
1423
  const rcsContent = {
1342
1424
  contentType,
1343
1425
  cardType,
1344
1426
  cardSettings,
1345
1427
  cardContent,
1346
1428
  };
1429
+ const cardVarMappedFromFirstRcsCard =
1430
+ firstCardFromSubmit?.cardVarMapped != null
1431
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1432
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1433
+ : null;
1434
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1435
+ cardVarMappedFromFirstRcsCard
1436
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1437
+ && isFullModeForRcsConsumerPayload === true;
1347
1438
  templateData = {
1348
1439
  channel,
1349
1440
  creativeName: name,
1350
1441
  rcsContent,
1351
1442
  accountId,
1443
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1444
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1445
+ : {}),
1352
1446
  };
1447
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1448
+ // so reopening the editor restores fallback text and tag mappings.
1449
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1450
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1451
+ const smsText =
1452
+ smsFallBackContent.message
1453
+ ?? smsFallBackContent.smsContent
1454
+ ?? smsFallBackContent.smsTemplateContent
1455
+ ?? '';
1456
+ templateData.smsFallBackContent = {
1457
+ ...smsFallBackContent,
1458
+ ...(String(smsText).trim() !== ''
1459
+ ? { message: String(smsText).trim() }
1460
+ : {}),
1461
+ };
1462
+ }
1463
+ normalizeRcsMessageContentForApi(templateData);
1353
1464
  }
1354
1465
  break;
1355
1466
  case constants.ZALO:
@@ -1467,7 +1578,10 @@ export class Creatives extends React.Component {
1467
1578
  return templateData;
1468
1579
  };
1469
1580
 
1470
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1581
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1582
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1583
+ return 'templates';
1584
+ }
1471
1585
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1472
1586
  if (mode === 'create' && isFullMode) {
1473
1587
  creativesMode = 'createTemplate';
@@ -1555,24 +1669,110 @@ export class Creatives extends React.Component {
1555
1669
  getFormData = (template) => {
1556
1670
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1557
1671
  // (e.g. when user fixes validation error by typing, we must not auto-close the slidebox)
1558
- this.setState({ isGetFormData: false });
1559
- if (template.validity) {
1560
- this.setState(
1561
- {},
1562
- () => {
1563
- const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1564
- const channel = templateData.type;
1565
- const creativesData = this.getCreativesData(channel, template, templateData);// convers data to consumer understandable format
1566
- creativesData.then((data) => {
1567
- this.logGTMEvent(channel, data);
1568
- this.processCentralCommsMetaId(channel, data);
1672
+ this.setState(
1673
+ (prevState) => {
1674
+ const next = { isGetFormData: false };
1675
+ if (!template.validity) {
1676
+ return next;
1677
+ }
1678
+ const baseTd = prevState.templateData || template;
1679
+ const channel = (
1680
+ baseTd?.type
1681
+ || template?.type
1682
+ || get(template, 'value.type')
1683
+ || ''
1684
+ ).toUpperCase();
1685
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1686
+ // (parent may not merge getCreativesData back into templateData).
1687
+ if (this.props.isFullMode === false && template.value) {
1688
+ if (channel === constants.RCS) {
1689
+ const smsFallBackFromPayload = get(
1690
+ template.value,
1691
+ 'versions.base.content.RCS.smsFallBackContent',
1692
+ );
1693
+ const rcsCardVarMappedFromPayload = get(
1694
+ template.value,
1695
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1696
+ );
1697
+ next.templateData = {
1698
+ ...baseTd,
1699
+ type: constants.RCS,
1700
+ name: template?.value?.name,
1701
+ versions: template?.value?.versions,
1702
+ ...(smsFallBackFromPayload != null
1703
+ && typeof smsFallBackFromPayload === 'object'
1704
+ && Object.keys(smsFallBackFromPayload).length > 0
1705
+ ? { smsFallBackContent: smsFallBackFromPayload }
1706
+ : {}),
1707
+ ...(rcsCardVarMappedFromPayload != null
1708
+ && typeof rcsCardVarMappedFromPayload === 'object'
1709
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1710
+ : {}),
1711
+ };
1712
+ if (template._id) {
1713
+ next.templateData._id = template._id;
1714
+ }
1715
+ } else if (channel === constants.SMS) {
1716
+ const submittedSmsTemplateValue = template?.value;
1717
+ const smsVersions =
1718
+ submittedSmsTemplateValue?.history != null
1719
+ ? submittedSmsTemplateValue
1720
+ : {
1721
+ base: submittedSmsTemplateValue?.base,
1722
+ history: submittedSmsTemplateValue?.base
1723
+ ? [submittedSmsTemplateValue.base]
1724
+ : [],
1725
+ };
1726
+ next.templateData = {
1727
+ ...baseTd,
1728
+ type: constants.SMS,
1729
+ name: baseTd?.name || 'Campaign message SMS content',
1730
+ versions: smsVersions,
1731
+ };
1732
+ if (template?._id) {
1733
+ next.templateData._id = template._id;
1734
+ }
1735
+ }
1736
+ }
1737
+ return next;
1738
+ },
1739
+ () => {
1740
+ if (!template.validity) {
1741
+ return;
1742
+ }
1743
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1744
+ const channelForConsumer = String(
1745
+ templateData.type
1746
+ || template.type
1747
+ || get(template, 'value.type')
1748
+ || '',
1749
+ ).toUpperCase();
1750
+ const creativesData = this.getCreativesData(
1751
+ channelForConsumer,
1752
+ template,
1753
+ this.state.templateData || template,
1754
+ );// convers data to consumer understandable format
1755
+ creativesData.then((data) => {
1756
+ this.logGTMEvent(channelForConsumer, data);
1757
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1758
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1569
1759
  });
1570
- },
1571
- );
1572
- }
1760
+ });
1761
+ },
1762
+ );
1573
1763
  }
1574
1764
 
1575
- processCentralCommsMetaId = (channel, creativesData) => {
1765
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1766
+ const { closeSlideBoxAfterSubmit = false } = options;
1767
+ const maybeCloseLibrarySlideBox = () => {
1768
+ if (
1769
+ closeSlideBoxAfterSubmit
1770
+ && this.props.isFullMode === false
1771
+ && typeof this.handleCloseSlideBox === 'function'
1772
+ ) {
1773
+ this.handleCloseSlideBox();
1774
+ }
1775
+ };
1576
1776
  // Create the payload for the centralcommnsmetaId API call
1577
1777
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1578
1778
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1598,6 +1798,7 @@ export class Creatives extends React.Component {
1598
1798
  if (result?.status?.code === 200) {
1599
1799
  setMetaData(result);
1600
1800
  this.props.getCreativesData(creativesData);
1801
+ maybeCloseLibrarySlideBox();
1601
1802
  } else {
1602
1803
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1603
1804
  }
@@ -1608,6 +1809,7 @@ export class Creatives extends React.Component {
1608
1809
  } else {
1609
1810
  // If not a loyalty module or different action, should work as usual
1610
1811
  this.props.getCreativesData(creativesData);
1812
+ maybeCloseLibrarySlideBox();
1611
1813
  }
1612
1814
  };
1613
1815
 
@@ -1640,7 +1842,9 @@ export class Creatives extends React.Component {
1640
1842
  }
1641
1843
  this.setState((prevState) => ({
1642
1844
  ...prevState,
1643
- templateData: undefined,
1845
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1846
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1847
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1644
1848
  showSlideBox: false,
1645
1849
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1646
1850
  isLiquidValidationError: false,
@@ -1843,21 +2047,17 @@ export class Creatives extends React.Component {
1843
2047
  }
1844
2048
 
1845
2049
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1846
- const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1847
- const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1848
- const hasLiquid = !isDeepEmpty(liquidMsgs);
1849
- const hasStandard = !isDeepEmpty(standardMsgs);
1850
- const isLiquidValidationError = hasLiquid || hasStandard;
1851
- // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1852
- const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
1853
- if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
1854
- return;
1855
- }
1856
- this.setState({
1857
- isLiquidValidationError,
1858
- liquidErrorMessage: errorMessagesFromFormBuilder,
1859
- activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
1860
- });
2050
+ const next = computeLiquidFooterUpdateFromFormBuilder(
2051
+ errorMessagesFromFormBuilder,
2052
+ this.state.liquidErrorMessage,
2053
+ currentFormBuilderTab,
2054
+ {
2055
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
2056
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
2057
+ },
2058
+ );
2059
+ if (next == null) return;
2060
+ this.setState(next);
1861
2061
  }
1862
2062
 
1863
2063
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -1980,6 +2180,11 @@ export class Creatives extends React.Component {
1980
2180
  inAppEditorType,
1981
2181
  htmlEditorValidationState,
1982
2182
  } = this.state;
2183
+ const useLocalTemplates = get(
2184
+ this.props,
2185
+ 'localTemplatesConfig.useLocalTemplates',
2186
+ get(this.props, 'useLocalTemplates', false),
2187
+ );
1983
2188
  const {
1984
2189
  isFullMode,
1985
2190
  creativesMode,
@@ -1998,7 +2203,6 @@ export class Creatives extends React.Component {
1998
2203
  smsRegister,
1999
2204
  enableNewChannels,
2000
2205
  eventContextTags,
2001
- waitEventContextTags = {},
2002
2206
  isLoyaltyModule,
2003
2207
  loyaltyMetaData = {},
2004
2208
  } = this.props;
@@ -2032,14 +2236,7 @@ export class Creatives extends React.Component {
2032
2236
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
2033
2237
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
2034
2238
 
2035
- // Calculate margin for header/content (always apply if there are errors, regardless of editor type)
2036
- const slideBoxWrapperMargin = (get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0 && get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0)
2037
- ? CAP_SPACE_64
2038
- : get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0
2039
- ? CAP_SPACE_56
2040
- : get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0
2041
- ? CAP_SPACE_32
2042
- : 0;
2239
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
2043
2240
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
2044
2241
 
2045
2242
  // Compute anonymous user type and channel restrictions
@@ -2068,7 +2265,10 @@ export class Creatives extends React.Component {
2068
2265
  <SlideBoxWrapper
2069
2266
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2070
2267
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2071
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2268
+ className={classnames(
2269
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2270
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2271
+ )}
2072
2272
  >
2073
2273
  <CapSlideBox
2074
2274
  header={
@@ -2093,12 +2293,13 @@ export class Creatives extends React.Component {
2093
2293
  smsRegister={smsRegister}
2094
2294
  handleClose={this.handleCloseSlideBox}
2095
2295
  moduleType={this.props.messageDetails?.type}
2296
+ useLocalTemplates={useLocalTemplates}
2096
2297
  />
2097
2298
  )}
2098
2299
  content={(
2099
2300
  <SlideBoxContent
2100
2301
  key="creatives-container-slidebox-content"
2101
- onSelectTemplate={this.onSelectTemplate}
2302
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2102
2303
  onCreateComplete={getCreativesData}
2103
2304
  onPreviewTemplate={this.onPreviewTemplate}
2104
2305
  slidBoxContent={slidBoxContent}
@@ -2166,7 +2367,6 @@ export class Creatives extends React.Component {
2166
2367
  creativesMode={creativesMode} // An existing prop that we're using here. Required to ensure correct account details in Edit or Preview in case of Embedded mode.
2167
2368
  hostName={this.props?.hostName || ''}
2168
2369
  eventContextTags={eventContextTags}
2169
- waitEventContextTags={waitEventContextTags}
2170
2370
  isLoyaltyModule={isLoyaltyModule}
2171
2371
  loyaltyMetaData={loyaltyMetaData}
2172
2372
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
@@ -2175,7 +2375,8 @@ export class Creatives extends React.Component {
2175
2375
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2176
2376
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2177
2377
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2178
- />
2378
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2379
+ />
2179
2380
  )}
2180
2381
  footer={this.shouldShowFooter() ? (
2181
2382
  <SlideBoxFooter
@@ -2260,13 +2461,31 @@ Creatives.propTypes = {
2260
2461
  orgUnitId: PropTypes.number,
2261
2462
  hostName: PropTypes.string,
2262
2463
  eventContextTags: PropTypes.array,
2263
- waitEventContextTags: PropTypes.object,
2264
2464
  loyaltyTagFetchingDependencies: PropTypes.object,
2265
2465
  customerType: PropTypes.string,
2266
2466
  intl: PropTypes.shape({
2267
2467
  formatMessage: PropTypes.func,
2268
2468
  }),
2269
2469
  stopValidation: PropTypes.func,
2470
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2471
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2472
+ localTemplatesConfig: PropTypes.shape({
2473
+ useLocalTemplates: PropTypes.bool,
2474
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2475
+ localTemplatesLoading: PropTypes.bool,
2476
+ localTemplatesFilterContent: PropTypes.node,
2477
+ localTemplatesSentinelContent: PropTypes.node,
2478
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2479
+ localTemplatesUseSkeleton: PropTypes.bool,
2480
+ }),
2481
+ useLocalTemplates: PropTypes.bool,
2482
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2483
+ localTemplatesLoading: PropTypes.bool,
2484
+ localTemplatesFilterContent: PropTypes.node,
2485
+ localTemplatesSentinelContent: PropTypes.node,
2486
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2487
+ localTemplatesUseSkeleton: PropTypes.bool,
2488
+ onSelectTemplate: PropTypes.func,
2270
2489
  };
2271
2490
  const mapStatesToProps = () => createStructuredSelector({
2272
2491
  isLoading: isLoadingSelector(),