@capillarytech/creatives-library 8.0.345-alpha.10 → 8.0.345-alpha.12

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 (127) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +13 -0
  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/templateVarUtils.test.js +204 -0
  8. package/v2Components/CapActionButton/constants.js +7 -0
  9. package/v2Components/CapActionButton/index.js +167 -109
  10. package/v2Components/CapActionButton/index.scss +157 -6
  11. package/v2Components/CapActionButton/messages.js +19 -3
  12. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  13. package/v2Components/CapTagList/index.js +10 -0
  14. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  21. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  22. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  26. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  27. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  28. package/v2Components/CommonTestAndPreview/index.js +676 -186
  29. package/v2Components/CommonTestAndPreview/messages.js +49 -3
  30. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  31. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  32. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  33. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  36. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  37. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  38. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  40. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  41. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  42. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  43. package/v2Components/FormBuilder/index.js +8 -10
  44. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  45. package/v2Components/SmsFallback/constants.js +73 -0
  46. package/v2Components/SmsFallback/index.js +955 -0
  47. package/v2Components/SmsFallback/index.scss +265 -0
  48. package/v2Components/SmsFallback/messages.js +78 -0
  49. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -0
  50. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  51. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  52. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
  55. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  56. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  57. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  58. package/v2Components/TemplatePreview/constants.js +2 -0
  59. package/v2Components/TemplatePreview/index.js +143 -28
  60. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  61. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  62. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  63. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  64. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  65. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  66. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  67. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  68. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -24
  69. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  70. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  71. package/v2Containers/CreativesContainer/constants.js +9 -0
  72. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  73. package/v2Containers/CreativesContainer/index.js +301 -107
  74. package/v2Containers/CreativesContainer/index.scss +51 -1
  75. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  77. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  81. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  82. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  83. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  84. package/v2Containers/Email/index.js +0 -23
  85. package/v2Containers/Rcs/constants.js +119 -8
  86. package/v2Containers/Rcs/index.js +2370 -807
  87. package/v2Containers/Rcs/index.js.rej +1336 -0
  88. package/v2Containers/Rcs/index.scss +276 -6
  89. package/v2Containers/Rcs/index.scss.rej +74 -0
  90. package/v2Containers/Rcs/messages.js +38 -3
  91. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  92. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98302 -70345
  93. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  94. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  95. package/v2Containers/Rcs/tests/index.test.js +152 -121
  96. package/v2Containers/Rcs/tests/mockData.js +38 -0
  97. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  98. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  99. package/v2Containers/Rcs/utils.js +478 -11
  100. package/v2Containers/Sms/Create/index.js +100 -40
  101. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  102. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  103. package/v2Containers/SmsTrai/Create/index.js +9 -4
  104. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  105. package/v2Containers/SmsTrai/Edit/index.js +636 -130
  106. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  107. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  108. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  109. package/v2Containers/SmsWrapper/index.js +37 -8
  110. package/v2Containers/TagList/index.js +6 -0
  111. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  112. package/v2Containers/Templates/_templates.scss +163 -2
  113. package/v2Containers/Templates/actions.js +11 -0
  114. package/v2Containers/Templates/constants.js +2 -0
  115. package/v2Containers/Templates/index.js +119 -54
  116. package/v2Containers/Templates/sagas.js +57 -12
  117. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  118. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  119. package/v2Containers/Templates/tests/sagas.test.js +193 -123
  120. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  121. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  122. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  123. package/v2Containers/TemplatesV2/index.js +86 -23
  124. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  125. package/v2Containers/Whatsapp/index.js +3 -20
  126. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  127. package/.npmrx +0 -2
@@ -1,9 +1,5 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import {
4
- CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
5
- } from '@capillarytech/cap-ui-library/styled/variables';
6
-
7
3
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
8
4
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
9
5
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
@@ -13,12 +9,11 @@ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
13
9
  import { injectIntl, FormattedMessage } from 'react-intl';
14
10
  import classnames from 'classnames';
15
11
  import {
16
- isEmpty, get, forEach, cloneDeep, debounce,
12
+ isEmpty, get, forEach, cloneDeep, debounce, pick,
17
13
  } from 'lodash';
18
14
  import { connect } from 'react-redux';
19
15
  import { createStructuredSelector } from 'reselect';
20
16
  import { bindActionCreators, compose } from 'redux';
21
- import styled from 'styled-components';
22
17
  import { GA } from '@capillarytech/cap-ui-utils';
23
18
  import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
24
19
 
@@ -32,6 +27,7 @@ import SlideBoxContent from './SlideBoxContent';
32
27
  import * as constants from './constants';
33
28
  import * as commonUtil from '../../utils/common';
34
29
  import { gtmPush } from '../../utils/gtmTrackers';
30
+ import { normalizeRcsMessageContentForApi } from '../../utils/rcsPayloadUtils';
35
31
  import './index.scss';
36
32
  import * as templateActions from '../Templates/actions';
37
33
  import * as globalActions from '../Cap/actions';
@@ -47,6 +43,9 @@ import {
47
43
  import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants';
48
44
  import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
49
45
  import { RCS_STATUSES } from '../Rcs/constants';
46
+ import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
47
+ import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
48
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
50
49
  import { CREATIVE } from '../Facebook/constants';
51
50
  import { LOYALTY } from '../App/constants';
52
51
  import {
@@ -61,6 +60,11 @@ import { capSagaForFetchSchemaForEntity, capSagaLiquidEntity } from '../Cap/saga
61
60
  import { v2TemplateSagaWatchGetDefaultBeeTemplates } from '../Templates/sagas';
62
61
  import { DYNAMIC_URL } from '../../v2Components/CapWhatsappCTA/constants';
63
62
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
63
+ import SlideBoxWrapper from './CreativesSlideBoxWrapper';
64
+ import {
65
+ computeLiquidFooterUpdateFromFormBuilder,
66
+ getSlideBoxWrapperMarginFromLiquidErrors,
67
+ } from './embeddedSlideboxUtils';
64
68
 
65
69
  import {
66
70
  transformChannelPayload,
@@ -69,51 +73,24 @@ import {
69
73
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
70
74
  import { BIG_HTML } from '../InApp/constants';
71
75
 
72
- /**
73
- * Returns true if value is "deep empty": no errors present.
74
- * - null/undefined: empty
75
- * - string: empty if length === 0
76
- * - array: empty if length === 0
77
- * - plain object (e.g. { android: [], ios: [], generic: [] }): empty only if every value is deep-empty
78
- */
79
- function isDeepEmpty(value) {
80
- if (value == null) return true;
81
- if (typeof value === 'string') return value.length === 0;
82
- if (Array.isArray(value)) return value.length === 0;
83
- if (typeof value === 'object') {
84
- return Object.values(value).every(isDeepEmpty);
85
- }
86
- return false;
87
- }
88
-
89
76
  const classPrefix = 'add-creatives-section';
90
77
  const CREATIVES_CONTAINER = 'creativesContainer';
91
78
 
92
- const SlideBoxWrapper = styled.div`
93
- .cap-slide-box-v2-container{
94
- .slidebox-header, .slidebox-content-container{
95
- margin-bottom: ${({ slideBoxWrapperMargin }) => `${slideBoxWrapperMargin}`};
96
- padding: 0 rem;
97
- &.has-footer{
98
- overflow-x: hidden;
99
- }
100
- }
101
- .slidebox-footer{
102
- /* Only apply margin-bottom to footer when ErrorInfoNote is shown in footer (BEE editor) */
103
- /* For HTML Editor, errors are shown in ValidationErrorDisplay (inside content area), so no footer margin needed */
104
- margin-bottom: ${({ shouldApplyFooterMargin }) => (shouldApplyFooterMargin ? `${CAP_SPACE_16}` : '0')};
105
- padding: 0 rem;
106
- &.has-footer{
107
- overflow-x: hidden;
108
- }
109
- }
110
- }
111
- `;
112
79
  export class Creatives extends React.Component {
113
80
  constructor(props) {
114
81
  super(props);
115
82
 
116
- const initialSlidBoxContent = this.getSlideBoxContent({ mode: props.creativesMode, templateData: props.templateData, isFullMode: props.isFullMode });
83
+ const useLocalTemplates = get(
84
+ props,
85
+ 'localTemplatesConfig.useLocalTemplates',
86
+ get(props, 'useLocalTemplates', false),
87
+ );
88
+ const initialSlidBoxContent = this.getSlideBoxContent({
89
+ mode: props.creativesMode,
90
+ templateData: props.templateData,
91
+ isFullMode: props.isFullMode,
92
+ useLocalTemplates,
93
+ });
117
94
 
118
95
  this.state = {
119
96
  isLoadingContent: true,
@@ -160,7 +137,13 @@ export class Creatives extends React.Component {
160
137
  }
161
138
 
162
139
  componentWillUnmount() {
163
- if (get(this.props, 'location.query.type', '') === "embedded") {
140
+ const isEmbedded = get(this.props, 'location.query.type', '') === "embedded";
141
+ const useLocalTemplates = get(
142
+ this.props,
143
+ 'localTemplatesConfig.useLocalTemplates',
144
+ get(this.props, 'useLocalTemplates', false),
145
+ );
146
+ if (isEmbedded && !useLocalTemplates) {
164
147
  this.props.templateActions.resetTemplateStoreData();
165
148
  }
166
149
  this.props.globalActions.clearMetaEntities();
@@ -762,25 +745,56 @@ export class Creatives extends React.Component {
762
745
  smsFallBackContent = {},
763
746
  creativeName = "",
764
747
  channel = constants.RCS,
765
- accountId = "",
748
+ rcsCardVarMapped,
766
749
  } = templateData || {};
767
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
750
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
751
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
752
+ const {
753
+ cardDisplayTitle: _omitDispTitleIn,
754
+ cardDisplayDescription: _omitDispDescIn,
755
+ ...cardContent
756
+ } = firstCardIn;
768
757
  const Status = RCS_STATUSES.approved || '';
758
+ const mergedCardVarMapped = (() => {
759
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
760
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
761
+ const nestedRecord =
762
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
763
+ ? nestedCardVarMapped
764
+ : {};
765
+ const rootRecord =
766
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
767
+ ? rootMirrorCardVarMapped
768
+ : {};
769
+ const mergedFromRootAndNested = {
770
+ ...pickRcsCardVarMappedEntries(rootRecord),
771
+ ...pickRcsCardVarMappedEntries(nestedRecord),
772
+ };
773
+ return Object.keys(mergedFromRootAndNested).length > 0
774
+ ? mergedFromRootAndNested
775
+ : null;
776
+ })();
777
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
778
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
779
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
780
+ const includeRootRcsCardVarMapped =
781
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
769
782
 
770
783
  creativesTemplateData = {
771
784
  type: channel,
772
785
  edit: true,
773
786
  name: creativeName,
787
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
774
788
  versions: {
775
789
  base: {
776
790
  content: {
777
791
  RCS: {
778
792
  rcsContent: {
779
793
  ...rcsContent,
780
- ...(accountId && !isFullMode && { accountId }),
781
794
  cardContent: [
782
795
  {
783
796
  ...cardContent,
797
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
784
798
  Status,
785
799
  },
786
800
  ],
@@ -931,7 +945,10 @@ export class Creatives extends React.Component {
931
945
  return newExpandableDetails;
932
946
  }
933
947
 
934
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
948
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
949
+ const channel = String(
950
+ channelParam || template?.type || get(template, 'value.type') || '',
951
+ ).toUpperCase();
935
952
  let templateData = { channel };
936
953
  switch (channel) {
937
954
  case constants.SMS:
@@ -966,13 +983,10 @@ export class Creatives extends React.Component {
966
983
  case constants.EMAIL:
967
984
  if (template?.value?.base) {
968
985
  let emailBase = template.value.base;
969
- const { html_content, drag_drop_id: formDragDropId } = emailBase || {};
986
+ const { html_content } = emailBase || {};
970
987
  if (!html_content) {
971
988
  emailBase = templateRecords.base;
972
989
  }
973
- if (!emailBase.drag_drop_id && formDragDropId) {
974
- emailBase = { ...emailBase, drag_drop_id: formDragDropId };
975
- }
976
990
  const newHtmlContent = await updateImagesInHtml(html_content);
977
991
  templateData = {
978
992
  ...templateData, ...emailBase, emailBody: newHtmlContent, emailSubject: (emailBase && emailBase.subject) ? emailBase.subject : '',
@@ -1228,7 +1242,7 @@ export class Creatives extends React.Component {
1228
1242
  };
1229
1243
  }
1230
1244
  break;
1231
- case constants.FACEBOOK:
1245
+ case constants.FACEBOOK: {
1232
1246
  if (template.value) {
1233
1247
  const FacebookAd = template?.value?.versions?.base?.content?.FacebookAd;
1234
1248
  const { type } = FacebookAd[0];
@@ -1272,34 +1286,110 @@ export class Creatives extends React.Component {
1272
1286
  selectedMarketingObjective: template.value.selectedMarketingObjective,
1273
1287
  };
1274
1288
  }
1289
+ }
1275
1290
  break;
1276
- case constants.RCS:
1291
+ case constants.RCS: {
1277
1292
  if (template.value) {
1278
- const { name = "", versions = {} } = {
1279
- } = template.value || {};
1280
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1293
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1294
+ const { name = "", versions = {} } = template.value || {};
1295
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1296
+ const fromRecords = {
1297
+ ...(templateRecords?.smsFallBackContent || {}),
1298
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1299
+ };
1300
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1301
+ if (
1302
+ !smsFallbackPayload
1303
+ || typeof smsFallbackPayload !== 'object'
1304
+ || Object.keys(smsFallbackPayload).length === 0
1305
+ ) {
1306
+ return false;
1307
+ }
1308
+ const fallbackBodyText = String(
1309
+ smsFallbackPayload.smsContent
1310
+ ?? smsFallbackPayload.smsTemplateContent
1311
+ ?? smsFallbackPayload.message
1312
+ ?? smsFallbackPayload.content
1313
+ ?? '',
1314
+ ).trim();
1315
+ const fallbackTemplateName = String(
1316
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1317
+ ).trim();
1318
+ const rcsSmsFallbackVarMapped =
1319
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1320
+ const hasVarMappedEntries =
1321
+ rcsSmsFallbackVarMapped != null
1322
+ && typeof rcsSmsFallbackVarMapped === 'object'
1323
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1324
+ return (
1325
+ fallbackBodyText !== ''
1326
+ || fallbackTemplateName !== ''
1327
+ || hasVarMappedEntries
1328
+ );
1329
+ };
1330
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1331
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1332
+ ? { ...fromRecords, ...fromSubmit }
1333
+ : { ...fromSubmit, ...fromRecords };
1281
1334
  const {
1282
- cardContent = [],
1335
+ cardContent: cardContentFromSubmit = [],
1283
1336
  contentType = "",
1284
1337
  cardType = "",
1285
1338
  cardSettings = {},
1286
- accountId = "",
1287
1339
  } = get(versions, 'base.content.RCS.rcsContent', {});
1340
+ const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1341
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1342
+ ? cardContentFromSubmit[0]
1343
+ : null;
1344
+ const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
1345
+ cardContentFromSubmit,
1346
+ rootRcsCardVarMappedFromSubmit,
1347
+ isFullModeForRcsConsumerPayload,
1348
+ );
1288
1349
  const rcsContent = {
1289
1350
  contentType,
1290
1351
  cardType,
1291
1352
  cardSettings,
1292
1353
  cardContent,
1293
1354
  };
1355
+ const cardVarMappedFromFirstRcsCard =
1356
+ firstCardFromSubmit?.cardVarMapped != null
1357
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1358
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1359
+ : null;
1360
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1361
+ cardVarMappedFromFirstRcsCard
1362
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1363
+ && isFullModeForRcsConsumerPayload === true;
1294
1364
  templateData = {
1295
1365
  channel,
1296
1366
  creativeName: name,
1297
1367
  rcsContent,
1298
- accountId,
1368
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1369
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1370
+ : {}),
1299
1371
  };
1372
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1373
+ // so reopening the editor restores fallback text and tag mappings.
1374
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1375
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1376
+ const smsText =
1377
+ smsFallBackContent.message
1378
+ ?? smsFallBackContent.smsContent
1379
+ ?? smsFallBackContent.smsTemplateContent
1380
+ ?? '';
1381
+ templateData.smsFallBackContent = {
1382
+ ...smsFallBackContent,
1383
+ ...(String(smsText).trim() !== ''
1384
+ ? { message: String(smsText).trim() }
1385
+ : {}),
1386
+ };
1387
+ }
1388
+ normalizeRcsMessageContentForApi(templateData);
1300
1389
  }
1390
+ }
1301
1391
  break;
1302
- case constants.ZALO:
1392
+ case constants.ZALO: {
1303
1393
  if (template.value) {
1304
1394
  templateData = {
1305
1395
  ...template.value,
@@ -1308,6 +1398,7 @@ export class Creatives extends React.Component {
1308
1398
  delete templateData.type;
1309
1399
  }
1310
1400
  }
1401
+ }
1311
1402
  break;
1312
1403
  case constants.WEBPUSH: {
1313
1404
  if (template.value) {
@@ -1414,7 +1505,10 @@ export class Creatives extends React.Component {
1414
1505
  return templateData;
1415
1506
  };
1416
1507
 
1417
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1508
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1509
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1510
+ return 'templates';
1511
+ }
1418
1512
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1419
1513
  if (mode === 'create' && isFullMode) {
1420
1514
  creativesMode = 'createTemplate';
@@ -1502,24 +1596,110 @@ export class Creatives extends React.Component {
1502
1596
  getFormData = (template) => {
1503
1597
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1504
1598
  // (e.g. when user fixes validation error by typing, we must not auto-close the slidebox)
1505
- this.setState({ isGetFormData: false });
1506
- if (template.validity) {
1507
- this.setState(
1508
- {},
1509
- () => {
1510
- const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1511
- const channel = templateData.type;
1512
- const creativesData = this.getCreativesData(channel, template, templateData);// convers data to consumer understandable format
1513
- creativesData.then((data) => {
1514
- this.logGTMEvent(channel, data);
1515
- this.processCentralCommsMetaId(channel, data);
1599
+ this.setState(
1600
+ (prevState) => {
1601
+ const next = { isGetFormData: false };
1602
+ if (!template.validity) {
1603
+ return next;
1604
+ }
1605
+ const baseTd = prevState.templateData || template;
1606
+ const channel = (
1607
+ baseTd?.type
1608
+ || template?.type
1609
+ || get(template, 'value.type')
1610
+ || ''
1611
+ ).toUpperCase();
1612
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1613
+ // (parent may not merge getCreativesData back into templateData).
1614
+ if (this.props.isFullMode === false && template.value) {
1615
+ if (channel === constants.RCS) {
1616
+ const smsFallBackFromPayload = get(
1617
+ template.value,
1618
+ 'versions.base.content.RCS.smsFallBackContent',
1619
+ );
1620
+ const rcsCardVarMappedFromPayload = get(
1621
+ template.value,
1622
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1623
+ );
1624
+ next.templateData = {
1625
+ ...baseTd,
1626
+ type: constants.RCS,
1627
+ name: template?.value?.name,
1628
+ versions: template?.value?.versions,
1629
+ ...(smsFallBackFromPayload != null
1630
+ && typeof smsFallBackFromPayload === 'object'
1631
+ && Object.keys(smsFallBackFromPayload).length > 0
1632
+ ? { smsFallBackContent: smsFallBackFromPayload }
1633
+ : {}),
1634
+ ...(rcsCardVarMappedFromPayload != null
1635
+ && typeof rcsCardVarMappedFromPayload === 'object'
1636
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1637
+ : {}),
1638
+ };
1639
+ if (template._id) {
1640
+ next.templateData._id = template._id;
1641
+ }
1642
+ } else if (channel === constants.SMS) {
1643
+ const submittedSmsTemplateValue = template?.value;
1644
+ const smsVersions =
1645
+ submittedSmsTemplateValue?.history != null
1646
+ ? submittedSmsTemplateValue
1647
+ : {
1648
+ base: submittedSmsTemplateValue?.base,
1649
+ history: submittedSmsTemplateValue?.base
1650
+ ? [submittedSmsTemplateValue.base]
1651
+ : [],
1652
+ };
1653
+ next.templateData = {
1654
+ ...baseTd,
1655
+ type: constants.SMS,
1656
+ name: baseTd?.name || 'Campaign message SMS content',
1657
+ versions: smsVersions,
1658
+ };
1659
+ if (template?._id) {
1660
+ next.templateData._id = template._id;
1661
+ }
1662
+ }
1663
+ }
1664
+ return next;
1665
+ },
1666
+ () => {
1667
+ if (!template.validity) {
1668
+ return;
1669
+ }
1670
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1671
+ const channelForConsumer = String(
1672
+ templateData.type
1673
+ || template.type
1674
+ || get(template, 'value.type')
1675
+ || '',
1676
+ ).toUpperCase();
1677
+ const creativesData = this.getCreativesData(
1678
+ channelForConsumer,
1679
+ template,
1680
+ this.state.templateData || template,
1681
+ );// convers data to consumer understandable format
1682
+ creativesData.then((data) => {
1683
+ this.logGTMEvent(channelForConsumer, data);
1684
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1685
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1516
1686
  });
1517
- },
1518
- );
1519
- }
1687
+ });
1688
+ },
1689
+ );
1520
1690
  }
1521
1691
 
1522
- processCentralCommsMetaId = (channel, creativesData) => {
1692
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1693
+ const { closeSlideBoxAfterSubmit = false } = options;
1694
+ const maybeCloseLibrarySlideBox = () => {
1695
+ if (
1696
+ closeSlideBoxAfterSubmit
1697
+ && this.props.isFullMode === false
1698
+ && typeof this.handleCloseSlideBox === 'function'
1699
+ ) {
1700
+ this.handleCloseSlideBox();
1701
+ }
1702
+ };
1523
1703
  // Create the payload for the centralcommnsmetaId API call
1524
1704
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1525
1705
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1545,6 +1725,7 @@ export class Creatives extends React.Component {
1545
1725
  if (result?.status?.code === 200) {
1546
1726
  setMetaData(result);
1547
1727
  this.props.getCreativesData(creativesData);
1728
+ maybeCloseLibrarySlideBox();
1548
1729
  } else {
1549
1730
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1550
1731
  }
@@ -1555,6 +1736,7 @@ export class Creatives extends React.Component {
1555
1736
  } else {
1556
1737
  // If not a loyalty module or different action, should work as usual
1557
1738
  this.props.getCreativesData(creativesData);
1739
+ maybeCloseLibrarySlideBox();
1558
1740
  }
1559
1741
  };
1560
1742
 
@@ -1587,7 +1769,9 @@ export class Creatives extends React.Component {
1587
1769
  }
1588
1770
  this.setState((prevState) => ({
1589
1771
  ...prevState,
1590
- templateData: undefined,
1772
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1773
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1774
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1591
1775
  showSlideBox: false,
1592
1776
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1593
1777
  isLiquidValidationError: false,
@@ -1798,21 +1982,12 @@ export class Creatives extends React.Component {
1798
1982
  }
1799
1983
 
1800
1984
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1801
- const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1802
- const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1803
- const hasLiquid = !isDeepEmpty(liquidMsgs);
1804
- const hasStandard = !isDeepEmpty(standardMsgs);
1805
- const isLiquidValidationError = hasLiquid || hasStandard;
1806
- // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1807
- const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
1808
- if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
1809
- return;
1810
- }
1811
- this.setState({
1812
- isLiquidValidationError,
1813
- liquidErrorMessage: errorMessagesFromFormBuilder,
1814
- activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
1985
+ const next = computeLiquidFooterUpdateFromFormBuilder(errorMessagesFromFormBuilder, currentFormBuilderTab, {
1986
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
1987
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
1815
1988
  });
1989
+ if (next == null) return;
1990
+ this.setState(next);
1816
1991
  }
1817
1992
 
1818
1993
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -1935,6 +2110,11 @@ export class Creatives extends React.Component {
1935
2110
  inAppEditorType,
1936
2111
  htmlEditorValidationState,
1937
2112
  } = this.state;
2113
+ const useLocalTemplates = get(
2114
+ this.props,
2115
+ 'localTemplatesConfig.useLocalTemplates',
2116
+ get(this.props, 'useLocalTemplates', false),
2117
+ );
1938
2118
  const {
1939
2119
  isFullMode,
1940
2120
  creativesMode,
@@ -1953,7 +2133,6 @@ export class Creatives extends React.Component {
1953
2133
  smsRegister,
1954
2134
  enableNewChannels,
1955
2135
  eventContextTags,
1956
- waitEventContextTags = {},
1957
2136
  isLoyaltyModule,
1958
2137
  loyaltyMetaData = {},
1959
2138
  } = this.props;
@@ -1987,14 +2166,7 @@ export class Creatives extends React.Component {
1987
2166
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
1988
2167
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
1989
2168
 
1990
- // Calculate margin for header/content (always apply if there are errors, regardless of editor type)
1991
- const slideBoxWrapperMargin = (get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0 && get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0)
1992
- ? CAP_SPACE_64
1993
- : get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0
1994
- ? CAP_SPACE_56
1995
- : get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0
1996
- ? CAP_SPACE_32
1997
- : 0;
2169
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
1998
2170
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
1999
2171
 
2000
2172
  // Compute anonymous user type and channel restrictions
@@ -2023,7 +2195,10 @@ export class Creatives extends React.Component {
2023
2195
  <SlideBoxWrapper
2024
2196
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2025
2197
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2026
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2198
+ className={classnames(
2199
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2200
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2201
+ )}
2027
2202
  >
2028
2203
  <CapSlideBox
2029
2204
  header={
@@ -2048,12 +2223,13 @@ export class Creatives extends React.Component {
2048
2223
  smsRegister={smsRegister}
2049
2224
  handleClose={this.handleCloseSlideBox}
2050
2225
  moduleType={this.props.messageDetails?.type}
2226
+ useLocalTemplates={useLocalTemplates}
2051
2227
  />
2052
2228
  )}
2053
2229
  content={(
2054
2230
  <SlideBoxContent
2055
2231
  key="creatives-container-slidebox-content"
2056
- onSelectTemplate={this.onSelectTemplate}
2232
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2057
2233
  onCreateComplete={getCreativesData}
2058
2234
  onPreviewTemplate={this.onPreviewTemplate}
2059
2235
  slidBoxContent={slidBoxContent}
@@ -2121,7 +2297,6 @@ export class Creatives extends React.Component {
2121
2297
  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.
2122
2298
  hostName={this.props?.hostName || ''}
2123
2299
  eventContextTags={eventContextTags}
2124
- waitEventContextTags={waitEventContextTags}
2125
2300
  isLoyaltyModule={isLoyaltyModule}
2126
2301
  loyaltyMetaData={loyaltyMetaData}
2127
2302
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
@@ -2130,7 +2305,8 @@ export class Creatives extends React.Component {
2130
2305
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2131
2306
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2132
2307
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2133
- />
2308
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2309
+ />
2134
2310
  )}
2135
2311
  footer={this.shouldShowFooter() ? (
2136
2312
  <SlideBoxFooter
@@ -2214,13 +2390,31 @@ Creatives.propTypes = {
2214
2390
  orgUnitId: PropTypes.number,
2215
2391
  hostName: PropTypes.string,
2216
2392
  eventContextTags: PropTypes.array,
2217
- waitEventContextTags: PropTypes.object,
2218
2393
  loyaltyTagFetchingDependencies: PropTypes.object,
2219
2394
  customerType: PropTypes.string,
2220
2395
  intl: PropTypes.shape({
2221
2396
  formatMessage: PropTypes.func,
2222
2397
  }),
2223
2398
  stopValidation: PropTypes.func,
2399
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2400
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2401
+ localTemplatesConfig: PropTypes.shape({
2402
+ useLocalTemplates: PropTypes.bool,
2403
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2404
+ localTemplatesLoading: PropTypes.bool,
2405
+ localTemplatesFilterContent: PropTypes.node,
2406
+ localTemplatesSentinelContent: PropTypes.node,
2407
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2408
+ localTemplatesUseSkeleton: PropTypes.bool,
2409
+ }),
2410
+ useLocalTemplates: PropTypes.bool,
2411
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2412
+ localTemplatesLoading: PropTypes.bool,
2413
+ localTemplatesFilterContent: PropTypes.node,
2414
+ localTemplatesSentinelContent: PropTypes.node,
2415
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2416
+ localTemplatesUseSkeleton: PropTypes.bool,
2417
+ onSelectTemplate: PropTypes.func,
2224
2418
  };
2225
2419
  const mapStatesToProps = () => createStructuredSelector({
2226
2420
  isLoading: isLoadingSelector(),