@capillarytech/creatives-library 9.0.28 → 9.0.29-alpha.2

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 (149) hide show
  1. package/constants/unified.js +29 -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/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +99 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +243 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +20 -1
  11. package/v2Components/CapActionButton/index.js +193 -144
  12. package/v2Components/CapActionButton/index.scss +232 -14
  13. package/v2Components/CapActionButton/messages.js +19 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +328 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/Email/index.js +3 -63
  96. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/carouselUtils.js +256 -0
  99. package/v2Containers/Rcs/components/CarouselCard.js +315 -0
  100. package/v2Containers/Rcs/components/CarouselCardButtons.js +93 -0
  101. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  102. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  103. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  104. package/v2Containers/Rcs/constants.js +137 -16
  105. package/v2Containers/Rcs/index.js +1912 -958
  106. package/v2Containers/Rcs/index.scss +443 -8
  107. package/v2Containers/Rcs/messages.js +45 -22
  108. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +522 -0
  109. package/v2Containers/Rcs/tests/CarouselCard.test.js +465 -0
  110. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  111. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  112. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  113. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  114. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  115. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  116. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  117. package/v2Containers/Rcs/tests/index.test.js +400 -40
  118. package/v2Containers/Rcs/tests/mockData.js +38 -0
  119. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
  120. package/v2Containers/Rcs/tests/utils.test.js +762 -30
  121. package/v2Containers/Rcs/utils.js +571 -12
  122. package/v2Containers/Sms/Create/index.js +115 -48
  123. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  124. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  125. package/v2Containers/SmsTrai/Create/index.js +9 -4
  126. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  127. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  128. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  129. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  130. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  131. package/v2Containers/SmsWrapper/index.js +37 -8
  132. package/v2Containers/TagList/index.js +5 -0
  133. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  134. package/v2Containers/Templates/_templates.scss +196 -12
  135. package/v2Containers/Templates/actions.js +11 -0
  136. package/v2Containers/Templates/constants.js +2 -0
  137. package/v2Containers/Templates/index.js +131 -59
  138. package/v2Containers/Templates/sagas.js +57 -13
  139. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  140. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  141. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  142. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  143. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  144. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  145. package/v2Containers/TemplatesV2/index.js +88 -25
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  147. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  148. package/v2Containers/Whatsapp/index.js +3 -20
  149. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -237,41 +237,6 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
237
237
  this.props.actions.getCmsSetting(BEE_PLUGIN, dragDropId, 'create', undefined, isBEESupport, isBEEAppEnable);
238
238
  }
239
239
  }
240
- // The <Email> direct path (SlideBoxContent routes here when
241
- // templateData.base.is_drag_drop is true) bypasses EmailWrapper/useEmailWrapper,
242
- // so Templates.BEETemplate is never seeded and a created/edited drag-drop would
243
- // open as the HTML editor. Seed isDragDrop + the BEE plugin from the inline
244
- // templateData so it opens as BEE.
245
- if (_.isEmpty(this.props.Templates.BEETemplate) && !_.isEmpty(this.props.templateData)) {
246
- const td = this.props.templateData;
247
- const activeTab = _.get(td, 'versions.base.activeTab', 'en');
248
- const inlineIsDragDrop = _.get(td, 'base.is_drag_drop')
249
- || _.get(td, `versions.base.${activeTab}.is_drag_drop`)
250
- || _.get(td, 'versions.base.is_drag_drop')
251
- || _.get(td, 'is_drag_drop');
252
- // BEE-ONLY: everything below is gated on is_drag_drop so this fallback
253
- // never touches the HTML/CKEditor flow (which also has an empty BEETemplate).
254
- if ((inlineIsDragDrop === true || inlineIsDragDrop === 1) && isBEEAppEnable) {
255
- this.setState({ isDragDrop: true });
256
- // This path bypasses setEditData, so seed the subject from the inline
257
- // templateData too (otherwise the title is empty for a created drag-drop).
258
- const inlineSubject = _.get(td, 'base.subject')
259
- || _.get(td, `versions.base.${activeTab}.subject`)
260
- || _.get(td, 'versions.base.subject')
261
- || _.get(td, 'emailSubject')
262
- || _.get(td, 'subject');
263
- if (inlineSubject) {
264
- formData['template-subject'] = inlineSubject;
265
- }
266
- const inlineDragDropId = _.get(td, 'base.drag_drop_id')
267
- || _.get(td, 'base.id')
268
- || _.get(td, `versions.base.${activeTab}.drag_drop_id`)
269
- || _.get(td, `versions.base.${activeTab}.id`)
270
- || _.get(td, 'drag_drop_id')
271
- || _.get(td, 'id');
272
- this.props.actions.getCmsSetting(BEE_PLUGIN, inlineDragDropId, this.props.params.id ? 'open' : 'create', undefined, isBEESupport, isBEEAppEnable);
273
- }
274
- }
275
240
  const hasUploadedContent = !this.props.params?.id && this.props.Templates?.selectedEmailLayout && !_.isEmpty(formData);
276
241
  this.setState({
277
242
  content: (this.props.Templates.selectedEmailLayout ? this.props.Templates.selectedEmailLayout : ''),
@@ -588,21 +553,10 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
588
553
  const beeToken = `BEEeditor${currentTab > 1 ? currentTab : ''}token`;
589
554
  let beeJsonValue = '';
590
555
  const selectedId = _.get(this.props, 'Email.templateDetails._id', '') || _.get(this.props, 'Templates.BEETemplate.versions.base.drag_drop_id', '');
591
-
592
- // Embedded journey content: the inline templateData carries the user's
593
- // edited BEE json (base.json_content), which is more current than the
594
- // reusable template fetched via getTemplateDetails. Prefer it so a
595
- // re-edit shows the saved edit instead of the original template. For
596
- // the create/gallery flow this key is absent, so we fall through to the
597
- // existing sources (template details / BEETemplate) unchanged.
598
- beeJsonValue = _.get(nextProps, 'templateData.base.json_content', '')
599
- || _.get(nextProps, 'templateData.json_content', '');
600
-
556
+
601
557
  // Get beeJsonValue from template data - check multiple sources
602
558
  // First check if it's already in formData (from setEditData)
603
- if (!beeJsonValue) {
604
- beeJsonValue = _.get(this.state, `formData[${currentTab - 1}][${langId}].json-content`, '');
605
- }
559
+ beeJsonValue = _.get(this.state, `formData[${currentTab - 1}][${langId}].json-content`, '');
606
560
 
607
561
  // Also check formData.base
608
562
  if (!beeJsonValue) {
@@ -1185,21 +1139,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
1185
1139
  const type = this.props.location.query.type;
1186
1140
 
1187
1141
  formData['template-name'] = editData.name;
1188
- // BEE-ONLY: for embedded drag-drop content, prefer the inline edited subject
1189
- // (base.subject / emailSubject) over the fetched template's subject so subject
1190
- // edits persist on re-open. Gated on is_drag_drop so the HTML/CKEditor flow is
1191
- // untouched (it keeps using editData.versions.base.subject as before).
1192
- // setEditData runs once (editDataSet), so this is a one-time load-time override.
1193
- const tdIsDragDrop = _.get(this.props, 'templateData.base.is_drag_drop') === true
1194
- || _.get(this.props, 'templateData.base.is_drag_drop') === 1
1195
- || _.get(this.props, 'templateData.is_drag_drop') === true
1196
- || _.get(this.props, 'templateData.is_drag_drop') === 1;
1197
- const inlineSubject = tdIsDragDrop
1198
- ? (_.get(this.props, 'templateData.base.subject', '')
1199
- || _.get(this.props, 'templateData.emailSubject', '')
1200
- || _.get(this.props, 'templateData.subject', ''))
1201
- : '';
1202
- const subject = inlineSubject || _.get(editData, 'versions.base.subject', '');
1142
+ const subject = _.get(editData, 'versions.base.subject', '');
1203
1143
  formData['template-subject'] = subject;
1204
1144
  formData.base = editData.versions.base;
1205
1145
 
@@ -234,18 +234,13 @@ const useEmailWrapper = ({
234
234
  // New flow: When template details are loaded and it's a BEE template, set it in Templates.BEETemplate
235
235
  // This allows Email component to properly initialize and call getCmsSetting
236
236
  const hasTemplateDetails = Email?.templateDetails && !isEmpty(Email.templateDetails);
237
- // Inline templateData (e.g. a freshly-created drag-drop content, or a
238
- // library add-creative -> select-template) is authoritative when no template
239
- // details were fetched. Without seeding BEETemplate from it, setBEETemplate
240
- // never fires for that flow and a BEE template opens as the HTML editor.
241
- const hasTemplateDataProp = templateData && !isEmpty(templateData);
242
237
  // Note: We check Email?.BEETemplate as a proxy, but the actual BEETemplate is in Templates reducer
243
238
  // The Email component will detect it via this.props.Templates.BEETemplate
244
239
  const hasBEETemplate = Email?.BEETemplate && !isEmpty(Email.BEETemplate);
245
240
 
246
- if ((hasTemplateDetails || hasTemplateDataProp) && !hasBEETemplate && templatesActions?.setBEETemplate && !beeTemplateSetRef.current) {
241
+ if (hasTemplateDetails && !hasBEETemplate && templatesActions?.setBEETemplate && !beeTemplateSetRef.current) {
247
242
  // Check if it's a BEE template
248
- const editTemplateData = hasTemplateDetails ? Email.templateDetails : templateData;
243
+ const editTemplateData = Email.templateDetails;
249
244
  const getIsDragDrop = (data) => {
250
245
  if (!data) return false;
251
246
  const baseDragDrop = get(data, 'versions.base.is_drag_drop', false);
@@ -301,10 +296,10 @@ const useEmailWrapper = ({
301
296
  }
302
297
 
303
298
  // Reset ref when template changes (template details cleared)
304
- if (!hasTemplateDetails && !hasTemplateDataProp && beeTemplateSetRef.current) {
299
+ if (!hasTemplateDetails && beeTemplateSetRef.current) {
305
300
  beeTemplateSetRef.current = false;
306
301
  }
307
- }, [Email?.templateDetails, Email?.BEETemplate, Email?.getTemplateDetailsInProgress, Email?.fetchingCmsSettings, templatesActions, emailActions, params?.id, location?.query, checkBeeEditorEnabled, isFullMode, templateData]);
302
+ }, [Email?.templateDetails, Email?.BEETemplate, Email?.getTemplateDetailsInProgress, Email?.fetchingCmsSettings, templatesActions, emailActions, params?.id, location?.query, checkBeeEditorEnabled, isFullMode]);
308
303
 
309
304
  const onChange = useCallback((e) => {
310
305
  const { target: { value } } = e;
@@ -7,12 +7,12 @@ import {
7
7
 
8
8
  describe("MPUSH Sagas", () => {
9
9
  describe("v2MobilePushCreateSagas Combined", () => {
10
- it.concurrent("should initialize all mpush-related watcher sagas without error", () => {
10
+ it("should initialize all mpush-related watcher sagas without error", () => {
11
11
  return expectSaga(v2MobilePushCreateSagas).run();
12
12
  });
13
13
  });
14
14
  describe("v2MobilePushWatchDuplicateTemplateSaga Combined", () => {
15
- it.concurrent("should initialize all mpush-related watcher sagas without error", () => {
15
+ it("should initialize all mpush-related watcher sagas without error", () => {
16
16
  return expectSaga(v2MobilePushWatchDuplicateTemplateSaga).run();
17
17
  });
18
18
  });
@@ -0,0 +1,256 @@
1
+ import cloneDeep from 'lodash/cloneDeep';
2
+ import { isUrl, isValidText } from '../Line/Container/Wrapper/utils';
3
+ import {
4
+ invalidVarRegex,
5
+ RCS_CTA_URL_TYPE,
6
+ URL_MAX_LENGTH,
7
+ } from '../../v2Components/CapActionButton/constants';
8
+ import {
9
+ RCS_MEDIA_TYPES,
10
+ RCS_BUTTON_TYPES,
11
+ TEMPLATE_TITLE_MAX_LENGTH,
12
+ RCS_RICH_CARD_MAX_LENGTH,
13
+ RCS_NUMERIC_VAR_TOKEN_REGEX,
14
+ RCS_CAROUSEL_ASSET_INDEX_BASE,
15
+ RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
16
+ MEDIUM,
17
+ } from './constants';
18
+ import { sanitizeCardVarMappedValue } from './utils';
19
+
20
+ const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
21
+
22
+ /**
23
+ * Assigns every variable token across the whole carousel a stable `cardVarMapped` key (a plain
24
+ * number, title then description, per card, in encounter order) — {cardIndex: {title: [key, …],
25
+ * description: [key, …]}}. Each occurrence is its own independent slot — even two tokens sharing
26
+ * the same literal tag name (e.g. "{{name}}" in both a card's title and description) get separate
27
+ * keys and are filled independently. A slot's key depends only on where its token sits in the
28
+ * template text, never on which tag currently fills it, so re-picking a different tag for the
29
+ * same slot never changes its key.
30
+ */
31
+ export const computeCarouselVarSlotKeys = (carouselData, rcsVarRegex) => {
32
+ let nextNumber = 1;
33
+ return (carouselData || []).map((card = {}) => {
34
+ const keysForField = (fieldStr) => (fieldStr || '').match(rcsVarRegex)?.map(() => {
35
+ const key = String(nextNumber);
36
+ nextNumber += 1;
37
+ return key;
38
+ }) ?? [];
39
+ return {
40
+ title: keysForField(card.title),
41
+ description: keysForField(card.description),
42
+ };
43
+ });
44
+ };
45
+
46
+ export const getCarouselImageAssetIndex = (cardIndex) =>
47
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
48
+ export const getCarouselVideoAssetIndex = (cardIndex) =>
49
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 1;
50
+ export const getCarouselThumbnailAssetIndex = (cardIndex) =>
51
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 2;
52
+
53
+ export const isThumbnailAssetIndex = (index) => {
54
+ if (index === 1) return true; // standalone thumbnail
55
+ if (index >= RCS_CAROUSEL_ASSET_INDEX_BASE) {
56
+ return ((index - RCS_CAROUSEL_ASSET_INDEX_BASE) % 3) === 2; // carousel thumbnail slot
57
+ }
58
+ return false;
59
+ };
60
+
61
+ /** Carousel dimension key: `${HEIGHT}_${WIDTH}` (e.g., SHORT_SMALL) */
62
+ export const getCarouselDimensionKey = (selectedCarouselHeight, selectedCarouselWidth) =>
63
+ `${selectedCarouselHeight}_${selectedCarouselWidth}`;
64
+
65
+ export const ensureFirstCardDefaultPhoneSuggestions = (cards) => {
66
+ const next = cloneDeep(cards || []);
67
+ if (next.length === 0) return next;
68
+ const suggestions = next[0].suggestions;
69
+ if (!Array.isArray(suggestions) || suggestions.length === 0) {
70
+ next[0] = {
71
+ ...next[0],
72
+ suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
73
+ };
74
+ }
75
+ return next;
76
+ };
77
+
78
+ export const carouselButtonTextHasForbiddenChars = (value) => {
79
+ if (!value) return false;
80
+ if (value.includes('[') || value.includes(']')) return true;
81
+ const withoutValidVariables = value.replace(/\{\{[^}]*\}\}/g, '');
82
+ if (withoutValidVariables.includes('{') || withoutValidVariables.includes('}')) return true;
83
+ return false;
84
+ };
85
+
86
+ export const isCompleteSavedCarouselSuggestion = (suggestion) => {
87
+ if (!suggestion || !suggestion.isSaved) return false;
88
+ const text = (suggestion.text || '').trim();
89
+ if (!text || !isValidText(text) || carouselButtonTextHasForbiddenChars(text)) return false;
90
+ if (suggestion.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
91
+ return String(suggestion.phoneNumber || '').length >= 5;
92
+ }
93
+ if (suggestion.type === RCS_BUTTON_TYPES.CTA) {
94
+ const url = String(suggestion.url || '').trim();
95
+ if (!url || url.length > URL_MAX_LENGTH) return false;
96
+ const subtype = suggestion.urlType || RCS_CTA_URL_TYPE.STATIC;
97
+ if (subtype === RCS_CTA_URL_TYPE.DYNAMIC) {
98
+ return true;
99
+ }
100
+ if (!isUrl(url)) return false;
101
+ const varMatches = url.match(invalidVarRegex);
102
+ return !(varMatches && varMatches.length > 0);
103
+ }
104
+ if (suggestion.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
105
+ return true;
106
+ }
107
+ return false;
108
+ };
109
+
110
+ export const isCarouselCardValid = (card, cardIndex) => {
111
+ if (!card) return false;
112
+ if (!card.title || !card.title.trim()) return false;
113
+ if ((card.title || '').length > TEMPLATE_TITLE_MAX_LENGTH) return false;
114
+ if (!card.description || !card.description.trim()) return false;
115
+ if ((card.description || '').length > RCS_RICH_CARD_MAX_LENGTH) return false;
116
+ let mediaOk = false;
117
+ if (card.mediaType === RCS_MEDIA_TYPES.IMAGE) {
118
+ mediaOk = !!(card.imageSrc && String(card.imageSrc).trim());
119
+ } else if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
120
+ const hasVideo = !!(card.videoAsset && card.videoAsset.videoSrc && String(card.videoAsset.videoSrc).trim());
121
+ const hasThumb = !!(card.thumbnailSrc && String(card.thumbnailSrc).trim());
122
+ mediaOk = hasVideo && hasThumb;
123
+ } else {
124
+ return false;
125
+ }
126
+ if (!mediaOk) return false;
127
+ if (cardIndex === 0) {
128
+ const sugg = Array.isArray(card.suggestions) ? card.suggestions : [];
129
+ if (!sugg.some(isCompleteSavedCarouselSuggestion)) return false;
130
+ }
131
+ return true;
132
+ };
133
+
134
+ export const checkDisableAddCarouselButton = (carouselData, activeCarouselIndex) => {
135
+ const idx = parseInt(activeCarouselIndex, 10);
136
+ const activeCard = carouselData?.[idx];
137
+ return !isCarouselCardValid(activeCard, idx);
138
+ };
139
+
140
+ /** Carousel: global variables across the whole carousel (all cards, title+body) */
141
+ export const getNextCarouselVarToken = (carouselData) => {
142
+ const nums = [];
143
+ (carouselData || []).forEach((card = {}) => {
144
+ const titleMatches = (card.title || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
145
+ const descMatches = (card.description || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
146
+ [...titleMatches, ...descMatches].forEach((tok) => {
147
+ const num = parseInt((tok.match(/\d+/) || [])[0], 10);
148
+ if (!Number.isNaN(num)) nums.push(num);
149
+ });
150
+ });
151
+ const existing = new Set(nums);
152
+ let nextNumber = 1;
153
+ while (existing.has(nextNumber)) nextNumber++;
154
+ if (nextNumber > 19) return '';
155
+ return `{{${nextNumber}}}`;
156
+ };
157
+
158
+ /** Carousel: per-card character counts (same limits as rich card) */
159
+ export const getCarouselTitleCharacterCount = (carouselData, cardIndex) => {
160
+ const titleText = carouselData?.[cardIndex]?.title || '';
161
+ return titleText ? titleText.length : 0;
162
+ };
163
+
164
+ export const getCarouselDescriptionCharacterCount = (carouselData, cardIndex) => {
165
+ const descriptionText = carouselData?.[cardIndex]?.description || '';
166
+ return descriptionText ? descriptionText.length : 0;
167
+ };
168
+
169
+ /**
170
+ * Preview (TestAndPreviewSlidebox / listing preview): resolve each card's title/desc through
171
+ * `resolveCarouselTemplateWithMap(str, cardIndex, isDescField)` — each variable slot's key is its
172
+ * global position across the whole carousel (see `getCarouselGlobalVarSlotKey`), so two cards can
173
+ * never collide even when they pick the same tag name for their own slot.
174
+ */
175
+ export const buildCarouselCardsForPreview = (cards = [], { isFullMode, resolveCarouselTemplateWithMap }) =>
176
+ (cards || []).map((card = {}, cardIndex) => {
177
+ const rawTitle = card.title || '';
178
+ const rawDesc = card.description || '';
179
+ const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
180
+ const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
181
+ const resolvedTitle = !isFullMode
182
+ ? resolveCarouselTemplateWithMap(rawTitle, cardIndex, false)
183
+ : rawTitle;
184
+ const resolvedDesc = !isFullMode
185
+ ? resolveCarouselTemplateWithMap(rawDesc, cardIndex, true)
186
+ : rawDesc;
187
+ if (!isFullMode) {
188
+ slotOffset += titleVarCount + descVarCount;
189
+ }
190
+ const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
191
+ const videoSrc = card.videoAsset?.videoSrc || '';
192
+ return {
193
+ mediaType: (card.mediaType || '').toLowerCase(),
194
+ imageSrc: card.imageSrc || '',
195
+ videoSrc,
196
+ videoPreviewImg: videoThumb,
197
+ title: resolvedTitle,
198
+ bodyText: resolvedDesc,
199
+ suggestions: card.suggestions || [],
200
+ };
201
+ });
202
+ };
203
+
204
+ /** Payload (create/edit RCS template): build `cardContent` for the carousel case. */
205
+ export const buildCarouselCardContentForPayload = (carouselData, {
206
+ isSlotMappingMode,
207
+ cardVarMapped,
208
+ selectedCarouselHeight,
209
+ rcsVarRegex,
210
+ }) => {
211
+ const slotKeysByCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex);
212
+ return (carouselData || []).map((card = {}, cardIndex) => {
213
+ const cardMediaType = card.mediaType;
214
+ const isCardVideo = cardMediaType === RCS_MEDIA_TYPES.VIDEO;
215
+ const isCardImage = cardMediaType === RCS_MEDIA_TYPES.IMAGE;
216
+ const mediaUrl = isCardVideo
217
+ ? (card.videoAsset?.videoSrc || '')
218
+ : (card.imageSrc || '');
219
+ const thumbnailUrl = isCardVideo
220
+ ? (card.thumbnailSrc || card.videoAsset?.videoThumbnail || '')
221
+ : '';
222
+ const cardVarTokens = [
223
+ ...((card.title || '').match(rcsVarRegex) ?? []),
224
+ ...((card.description || '').match(rcsVarRegex) ?? []),
225
+ ];
226
+ const cardSlotKeys = [
227
+ ...(slotKeysByCard[cardIndex]?.title ?? []),
228
+ ...(slotKeysByCard[cardIndex]?.description ?? []),
229
+ ];
230
+ const cardVarMappedForCard = {};
231
+ cardVarTokens.forEach((token, tokenIndex) => {
232
+ const varName = getVarNameFromToken(token);
233
+ const slotKey = cardSlotKeys[tokenIndex];
234
+ if (!varName || !isSlotMappingMode) return;
235
+ const scopedValue = cardVarMapped?.[slotKey] ?? '';
236
+ cardVarMappedForCard[slotKey] = sanitizeCardVarMappedValue(scopedValue);
237
+ });
238
+ return {
239
+ title: card.title || '',
240
+ description: card.description || '',
241
+ mediaType: cardMediaType,
242
+ ...((isCardImage || isCardVideo) && {
243
+ media: {
244
+ mediaUrl,
245
+ thumbnailUrl,
246
+ height: selectedCarouselHeight || MEDIUM,
247
+ ...(isCardVideo && card.videoAsset?.videoName && { videoName: card.videoAsset.videoName }),
248
+ },
249
+ }),
250
+ ...(Array.isArray(card.suggestions) && card.suggestions.length > 0 && {
251
+ suggestions: card.suggestions,
252
+ }),
253
+ ...(Object.keys(cardVarMappedForCard).length > 0 && { cardVarMapped: cardVarMappedForCard }),
254
+ };
255
+ });
256
+ };