@capillarytech/creatives-library 8.0.345-alpha.14 → 8.0.345-alpha.15

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 (129) 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 +36 -4
  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 +300 -103
  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/reducer.js +3 -11
  85. package/v2Containers/Email/sagas.js +5 -9
  86. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +0 -4
  87. package/v2Containers/Email/tests/sagas.test.js +3 -21
  88. package/v2Containers/Rcs/constants.js +119 -8
  89. package/v2Containers/Rcs/index.js +2379 -807
  90. package/v2Containers/Rcs/index.js.rej +1336 -0
  91. package/v2Containers/Rcs/index.scss +276 -6
  92. package/v2Containers/Rcs/index.scss.rej +74 -0
  93. package/v2Containers/Rcs/messages.js +38 -3
  94. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  95. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  96. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  97. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  98. package/v2Containers/Rcs/tests/index.test.js +152 -121
  99. package/v2Containers/Rcs/tests/mockData.js +38 -0
  100. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  101. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  102. package/v2Containers/Rcs/utils.js +478 -11
  103. package/v2Containers/Sms/Create/index.js +100 -40
  104. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  105. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  106. package/v2Containers/SmsTrai/Create/index.js +9 -4
  107. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  108. package/v2Containers/SmsTrai/Edit/index.js +636 -130
  109. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  110. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  111. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  112. package/v2Containers/SmsWrapper/index.js +37 -8
  113. package/v2Containers/TagList/index.js +6 -0
  114. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  115. package/v2Containers/Templates/_templates.scss +163 -2
  116. package/v2Containers/Templates/actions.js +11 -0
  117. package/v2Containers/Templates/constants.js +2 -0
  118. package/v2Containers/Templates/index.js +119 -54
  119. package/v2Containers/Templates/sagas.js +57 -12
  120. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  121. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  122. package/v2Containers/Templates/tests/sagas.test.js +193 -123
  123. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  124. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  125. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  126. package/v2Containers/TemplatesV2/index.js +86 -23
  127. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  128. package/v2Containers/Whatsapp/index.js +3 -20
  129. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,92 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+
3
+ /**
4
+ * @param {Object} options
5
+ * @param {(params: { page: number, search: string, reset: boolean }) => Promise<{ templates: Array, totalCount: number }>} options.fetchTemplates
6
+ * @param {number} [options.perPage=25]
7
+ */
8
+ export function useLocalTemplateList({ fetchTemplates, perPage = 25 }) {
9
+ const [listData, setListData] = useState({ templates: [], totalCount: 0 });
10
+ const [loading, setLoading] = useState(false);
11
+ const [page, setPage] = useState(1);
12
+ const [search, setSearchState] = useState('');
13
+ const searchRef = useRef('');
14
+ /** Drops stale responses when a newer fetch starts (search / reset while a request is in flight). */
15
+ const fetchGenerationRef = useRef(0);
16
+ const setSearch = useCallback((value) => {
17
+ const term = typeof value === 'string' ? value : '';
18
+ searchRef.current = term;
19
+ setSearchState(term);
20
+ }, []);
21
+ const lastFetchFullPageRef = useRef(false);
22
+
23
+ const { templates = [], totalCount = 0 } = listData ?? {};
24
+ const hasKnownTotal = (totalCount ?? 0) > 0;
25
+ const hasMoreByTotal = (totalCount ?? 0) > (templates?.length ?? 0);
26
+ const hasMoreByFullPage =
27
+ !hasKnownTotal && lastFetchFullPageRef.current && (templates?.length ?? 0) > 0;
28
+ const canLoadMore = (hasMoreByTotal || hasMoreByFullPage) && !loading;
29
+
30
+ const runFetch = useCallback(
31
+ async ({ page: p = 1, reset = true, search: searchTerm } = {}) => {
32
+ const term = searchTerm !== undefined ? searchTerm : searchRef.current;
33
+ const gen = ++fetchGenerationRef.current;
34
+ setLoading(true);
35
+ try {
36
+ const result = await fetchTemplates({ page: p, search: term, reset });
37
+ if (gen !== fetchGenerationRef.current) {
38
+ return;
39
+ }
40
+ const nextTemplates = result?.templates ?? [];
41
+ const nextTotalCount = result?.totalCount ?? 0;
42
+ lastFetchFullPageRef.current = nextTemplates.length >= perPage;
43
+ setListData((prev) => ({
44
+ templates: reset ? nextTemplates : [...(prev.templates || []), ...nextTemplates],
45
+ totalCount: nextTotalCount > 0 ? nextTotalCount : (reset ? 0 : prev.totalCount),
46
+ }));
47
+ setPage(p);
48
+ } catch (e) {
49
+ if (gen !== fetchGenerationRef.current) {
50
+ return;
51
+ }
52
+ lastFetchFullPageRef.current = false;
53
+ if (reset) {
54
+ setListData({ templates: [], totalCount: 0 });
55
+ setPage(1);
56
+ }
57
+ } finally {
58
+ if (gen === fetchGenerationRef.current) {
59
+ setLoading(false);
60
+ }
61
+ }
62
+ },
63
+ [fetchTemplates, perPage]
64
+ );
65
+
66
+ const loadMore = useCallback(() => {
67
+ if (!canLoadMore) return;
68
+ runFetch({ page: page + 1, reset: false, search: searchRef.current });
69
+ }, [canLoadMore, page, runFetch]);
70
+
71
+ const reset = useCallback(
72
+ (searchTerm) => {
73
+ const term = searchTerm !== undefined ? searchTerm : searchRef.current;
74
+ setSearch(term);
75
+ lastFetchFullPageRef.current = false;
76
+ runFetch({ page: 1, reset: true, search: term });
77
+ },
78
+ [runFetch, setSearch]
79
+ );
80
+
81
+ return {
82
+ templates,
83
+ totalCount,
84
+ loading,
85
+ page,
86
+ search,
87
+ setSearch,
88
+ loadMore,
89
+ reset,
90
+ canLoadMore,
91
+ };
92
+ }
@@ -537,20 +537,21 @@
537
537
  .unicode-disabled{
538
538
  font-size: 16px;
539
539
  }
540
+ position: absolute;
541
+ overflow: auto;
542
+ top: 0;
543
+ left: 17%;
544
+ white-space: pre-wrap;
545
+ word-break: break-word;
546
+ max-height: 100%;
547
+ line-height: 14px;
548
+ font-size: 10px;
549
+ font-family: 'open-sans';
550
+
540
551
  &.sms {
541
- max-height: 100%;
542
- position: absolute;
543
552
  // width: initial;
544
- overflow: auto;
545
- top: 0;
546
- left: 17%;
547
- white-space: pre-wrap;
548
- word-break: break-word;
549
- line-height: 14px;
550
553
  padding: 0 8px 8px 8px;
551
- font-size: 10px;
552
554
  width: 100%;
553
- font-family: 'open-sans';
554
555
  .rcs-image{
555
556
  width: 100%;
556
557
  height: 123px;
@@ -582,22 +583,12 @@
582
583
  &:not(.sms){
583
584
  padding: 4px;
584
585
  background: #3F51B5;
585
- position: absolute;
586
586
  // width: initial;
587
- overflow: auto;
588
- top: 0;
589
- left: 17%;
590
- white-space: pre-wrap;
591
- word-break: break-word;
592
587
  border-radius: 4px;
593
- max-height: 100%;
594
588
  color: #FFFFFF;
595
589
  width: 70%;
596
590
  min-height: 26px;
597
- line-height: 14px;
598
591
  padding: 8px;
599
- font-size: 10px;
600
- font-family: 'open-sans';
601
592
  }
602
593
  &.message-pop-carousel {
603
594
  position: relative;
@@ -842,6 +833,27 @@
842
833
  }
843
834
  }
844
835
 
836
+ .shell-v2.rcs-preview {
837
+ // Override `.message-pop:not(.sms)` (blue background) for RCS carousel cards.
838
+ .msg-container.sms {
839
+ .message-pop.sms {
840
+ .rcs-carousel-card {
841
+ background: $CAP_WHITE;
842
+ color: $CAP_G01;
843
+ width: 10.4rem;
844
+ left: 0;
845
+ flex-shrink: 0;
846
+ padding: $CAP_SPACE_04 0 $CAP_SPACE_08;
847
+ border-radius: 0.428rem;
848
+
849
+ .carousel-title {
850
+ font-weight: 700 !important;
851
+ }
852
+ }
853
+ }
854
+ }
855
+ }
856
+
845
857
  .align-center {
846
858
  text-align: center;
847
859
  }
@@ -1012,9 +1024,7 @@
1012
1024
  top: 0;
1013
1025
  }
1014
1026
  .video-icon {
1015
- position: absolute;
1016
- right: -17px;
1017
- bottom: -17px;
1027
+ position: sticky;
1018
1028
  }
1019
1029
 
1020
1030
  .zalo-preview-container {
@@ -0,0 +1,2 @@
1
+ /** Matches {{ varName }} placeholders (alphanumeric + underscore) */
2
+ export const TEMPLATE_VAR_REGEX = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
@@ -52,6 +52,7 @@ import { QUICK_REPLY, WHATSAPP_CATEGORIES, PHONE_NUMBER } from '../../v2Containe
52
52
  import { RCS_BUTTON_TYPES, LEFT, HORIZONTAL, VERTICAL, RIGHT} from '../../v2Containers/Rcs/constants';
53
53
  import { ANDROID, INAPP_MESSAGE_LAYOUT_TYPES } from '../../v2Containers/InApp/constants';
54
54
  import { CAROUSEL } from '../../v2Containers/MobilePushNew/constants';
55
+ import { TEMPLATE_VAR_REGEX } from './constants';
55
56
 
56
57
  const wechatBodyNew = require('./assets/images/wechat_mobile_android.svg');
57
58
  const smsMobileAndroid = require('./assets/images/sms_mobile_android.svg');
@@ -234,7 +235,23 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
234
235
  let content = channel && channel.toLowerCase() === 'sms' ? [this.props.content] : this.props.content;
235
236
  const { formatMessage } = intl;
236
237
  const { rcsPreviewContent, inAppPreviewContent, viberPreviewContent, isBeeFreeTemplate } = content || {};
237
- const { rcsImageSrc, rcsVideoSrc, rcsTitle, rcsDesc, rcsSuggestions } = rcsPreviewContent || {};
238
+ const normalizedRcsPreviewContent = Array.isArray(rcsPreviewContent)
239
+ ? { carouselData: rcsPreviewContent }
240
+ : (rcsPreviewContent || {});
241
+ const {
242
+ rcsImageSrc,
243
+ rcsVideoSrc,
244
+ rcsTitle,
245
+ rcsDesc,
246
+ rcsSuggestions,
247
+ carouselData: rcsCarouselData,
248
+ rcsCarouselData: rcsCarouselDataAlt,
249
+ cardVarMapped: rcsCardVarMapped,
250
+ } = normalizedRcsPreviewContent;
251
+ const resolvedRcsCarouselData =
252
+ Array.isArray(rcsCarouselData) && rcsCarouselData.length > 0
253
+ ? rcsCarouselData
254
+ : rcsCarouselDataAlt;
238
255
  const {
239
256
  videoParams,
240
257
  imageURL,
@@ -315,6 +332,18 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
315
332
  'flex-shrink': 0,
316
333
  'left': 0,
317
334
  };
335
+
336
+ const resolveVarsWithMap = (input, varMap) => {
337
+ if (input === null || input === undefined) return '';
338
+ const str = typeof input === 'string' ? input : String(input);
339
+ return str.replace(TEMPLATE_VAR_REGEX, (token, varName) => {
340
+ const mappedValue = varMap?.[varName];
341
+ if (mappedValue === null || mappedValue === undefined || String(mappedValue) === '') {
342
+ return token;
343
+ }
344
+ return String(mappedValue);
345
+ });
346
+ };
318
347
  const getVideoContent = ({
319
348
  video,
320
349
  actionUrl,
@@ -471,13 +500,15 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
471
500
  const whatsappUpdatedAccountName = whatsappAccountName || templateData?.versions?.base?.content?.whatsapp?.accountName || '';
472
501
  const whatsappUpdatedLen = whatsappContentLen !== undefined ? whatsappContentLen : content?.charCount;
473
502
 
474
- const renderRcsSuggestionsPreview = () => {
503
+ const renderRcsSuggestionsPreview = (suggestionsArg) => {
475
504
  const renderArray = [];
476
- (rcsSuggestions || []).forEach((suggestion) => {
477
- renderArray.push(<CapDivider className="whatsapp-divider" />);
505
+ const suggestions = Array.isArray(suggestionsArg) ? suggestionsArg : (rcsSuggestions || []);
506
+ (suggestions || []).forEach((suggestion, idx) => {
507
+ const suggestionKey = `${suggestion?.type || 'unknown'}-${suggestion?.text || ''}-${idx}`;
508
+ renderArray.push(<CapDivider key={`rcs-divider-${suggestionKey}`} className="whatsapp-divider" />);
478
509
  if (suggestion.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
479
510
  renderArray.push(
480
- <CapLabel type="label21" className="rcs-cta-preview">
511
+ <CapLabel key={`rcs-suggestion-${suggestionKey}`} type="label21" className="rcs-cta-preview">
481
512
  <CapIcon
482
513
  type='small-link'
483
514
  size="xs"
@@ -487,14 +518,14 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
487
518
  );
488
519
  } else if (suggestion.type === RCS_BUTTON_TYPES.CTA) {
489
520
  renderArray.push(
490
- <CapLabel type="label21" className="rcs-cta-preview">
521
+ <CapLabel key={`rcs-suggestion-${suggestionKey}`} type="label21" className="rcs-cta-preview">
491
522
  <CapIcon type="launch" size="xs" />
492
523
  {suggestion.text}
493
524
  </CapLabel>,
494
525
  );
495
526
  } else if (suggestion.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
496
527
  renderArray.push(
497
- <CapLabel type="label21" className="rcs-cta-preview">
528
+ <CapLabel key={`rcs-suggestion-${suggestionKey}`} type="label21" className="rcs-cta-preview">
498
529
  <CapIcon type="call" size="xs" />
499
530
  {suggestion.text}
500
531
  </CapLabel>,
@@ -513,7 +544,7 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
513
544
  className="message-pop-item align-left rcs-content"
514
545
  fontWeight="bold"
515
546
  >
516
- {rcsTitle}
547
+ {resolveVarsWithMap(rcsTitle, rcsCardVarMapped)}
517
548
  </CapLabel>
518
549
  <CapDivider className="whatsapp-divider" />
519
550
  </>
@@ -523,7 +554,7 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
523
554
  type="label5"
524
555
  className="message-pop-item align-left rcs-desc rcs-content"
525
556
  >
526
- {rcsDesc}
557
+ {resolveVarsWithMap(rcsDesc, rcsCardVarMapped)}
527
558
  </CapLabel>
528
559
  )}
529
560
  {renderRcsSuggestionsPreview()}
@@ -540,50 +571,134 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
540
571
  />
541
572
  )}
542
573
  {rcsVideoSrc && (
543
- <div className="video-preview">
574
+ <CapRow className="video-preview">
544
575
  <CapImage
545
576
  src={rcsVideoSrc}
546
577
  className="rcs-image"
547
578
  alt={formatMessage(messages.previewGenerated)}
548
579
  />
549
- <div className="icon-position">
580
+ <CapRow className="icon-position">
550
581
  <CapImage
551
582
  className="video-icon"
552
583
  src={videoPlay}
553
584
  />
554
- </div>
555
- </div>
585
+ </CapRow>
586
+ </CapRow>
556
587
  )}
557
588
  </>
558
589
  );
559
590
 
560
591
  const renderRcsPreviewContent = () => {
592
+ const carouselCards = Array.isArray(resolvedRcsCarouselData) ? resolvedRcsCarouselData : [];
593
+ if (carouselCards.length > 0) {
594
+ return (
595
+ <CapRow className="msg-container sms">
596
+ <CapRow className="message-pop sms">
597
+ <CapRow className="msg-container-carousel">
598
+ <CapRow className="scroll-container">
599
+ {carouselCards.map((card, idx) => {
600
+ const key = `rcs-carousel-${idx}-${card?.bodyText || card?.imageSrc || card?.videoPreviewImg || ''}`;
601
+ const isVideo = (card?.mediaType || '').toLowerCase() === 'video';
602
+ const cardSuggestions = Array.isArray(card?.suggestions) ? card.suggestions : [];
603
+ const effectiveCardVarMap = card?.cardVarMapped || rcsCardVarMapped;
604
+ const suggestionsNode = cardSuggestions.length > 0
605
+ ? renderRcsSuggestionsPreview(cardSuggestions)
606
+ : null;
607
+
608
+ const resolvedCardTitle = resolveVarsWithMap(card?.title, effectiveCardVarMap);
609
+ const resolvedCardBodyText = resolveVarsWithMap(card?.bodyText, effectiveCardVarMap);
610
+
611
+ return (
612
+ <CapRow
613
+ key={key}
614
+ className="message-pop align-left message-pop-carousel rcs-carousel-card"
615
+ >
616
+ <CapRow className="whatsapp-content">
617
+ {!isVideo && (
618
+ <CapImage
619
+ src={card?.imageSrc ? card.imageSrc : whatsappImageEmptyPreview}
620
+ className="whatsapp-image"
621
+ alt={formatMessage(messages.previewGenerated)}
622
+ />
623
+ )}
624
+ {isVideo && (
625
+ <CapTooltip title={formatMessage(messages.videoPreviewTooltip)}>
626
+ <CapRow className="video-preview">
627
+ <CapImage
628
+ src={card?.videoPreviewImg ? card.videoPreviewImg : whatsappVideoEmptyPreview}
629
+ className="whatsapp-image"
630
+ alt={formatMessage(messages.previewGenerated)}
631
+ />
632
+ <CapRow className="icon-position">
633
+ <CapImage className="video-icon" src={videoPlay} alt="Play" />
634
+ </CapRow>
635
+ </CapRow>
636
+ </CapTooltip>
637
+ )}
638
+
639
+ {(resolvedCardTitle || resolvedCardBodyText) && (
640
+ <CapRow className="carousel-content">
641
+ {!!resolvedCardTitle && (
642
+ <CapLabel
643
+ type={card?.titleLabelType || 'label1'}
644
+ className="carousel-title"
645
+ >
646
+ {resolvedCardTitle}
647
+ </CapLabel>
648
+ )}
649
+ {!!resolvedCardBodyText && (
650
+ <CapLabel
651
+ type={card?.bodyLabelType || 'label2'}
652
+ className="carousel-message"
653
+ >
654
+ {resolvedCardBodyText}
655
+ </CapLabel>
656
+ )}
657
+ </CapRow>
658
+ )}
659
+
660
+ {!!suggestionsNode && (
661
+ <>
662
+ {suggestionsNode}
663
+ </>
664
+ )}
665
+ </CapRow>
666
+ </CapRow>
667
+ );
668
+ })}
669
+ </CapRow>
670
+ </CapRow>
671
+ </CapRow>
672
+ </CapRow>
673
+ );
674
+ }
675
+
561
676
  if (rcsOrientation === HORIZONTAL) {
562
677
  return rcsType === RIGHT ? (
563
- <div className="msg-container sms">
564
- <div className="message-pop sms horizontal">
678
+ <CapRow className="msg-container sms">
679
+ <CapRow className="message-pop sms horizontal">
565
680
  <CapColumn className="rcs-preview-text" span={12}>{renderTextPreviewContent()}</CapColumn>
566
681
  <CapColumn span={12}>{renderMediaPreviewContent()}</CapColumn>
567
- </div>
568
- </div>
682
+ </CapRow>
683
+ </CapRow>
569
684
 
570
685
  ) : (
571
- <div className="msg-container sms">
572
- <div className="message-pop sms horizontal">
686
+ <CapRow className="msg-container sms">
687
+ <CapRow className="message-pop sms horizontal">
573
688
  <CapColumn span={12}>{renderMediaPreviewContent()}</CapColumn>
574
689
  <CapColumn className="rcs-preview-text" span={12}>{renderTextPreviewContent()}</CapColumn>
575
- </div>
576
- </div>
690
+ </CapRow>
691
+ </CapRow>
577
692
  );
578
693
  }
579
694
 
580
695
  return (
581
- <div className="msg-container sms">
582
- <div className="message-pop sms">
696
+ <CapRow className="msg-container sms">
697
+ <CapRow className="message-pop sms">
583
698
  {renderMediaPreviewContent()}
584
699
  {renderTextPreviewContent()}
585
- </div>
586
- </div>
700
+ </CapRow>
701
+ </CapRow>
587
702
  );
588
703
  };
589
704
 
@@ -1291,14 +1406,14 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
1291
1406
  ""
1292
1407
  )}
1293
1408
  {channel?.toUpperCase() === RCS && (
1294
- <div className="shell-v2 align-center rcs-preview">
1409
+ <CapRow className="shell-v2 align-center rcs-preview">
1295
1410
  <CapImage
1296
1411
  className="preview-image"
1297
1412
  src={rcsIosPreview ? smsMobileIos : smsMobileAndroid}
1298
1413
  alt={formatMessage(messages.previewGenerated)}
1299
1414
  />
1300
1415
  {renderRcsPreviewContent()}
1301
- </div>
1416
+ </CapRow>
1302
1417
 
1303
1418
  )}
1304
1419
  {channel?.toUpperCase() === ZALO && (
@@ -1499,4 +1614,4 @@ TemplatePreview.propTypes = {
1499
1614
  rcsOrientation: PropTypes.string,
1500
1615
  };
1501
1616
 
1502
- export default (injectIntl(TemplatePreview));
1617
+ export default (injectIntl(TemplatePreview));
@@ -2,6 +2,9 @@ import React from 'react';
2
2
  import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
3
3
 
4
4
  import { TemplatePreview } from '../index';
5
+ import { RCS } from '../../../v2Containers/CreativesContainer/constants';
6
+ import whatsappImageEmptyPreview from '../assets/images/empty_image_preview.svg';
7
+ import whatsappVideoEmptyPreview from '../assets/images/empty_video_preview.svg';
5
8
 
6
9
  describe('Test Templates container', () => {
7
10
  let renderedComponent;
@@ -69,4 +72,143 @@ describe('Test Templates container', () => {
69
72
  });
70
73
  expect(renderedComponent).toMatchSnapshot();
71
74
  });
75
+
76
+ describe('RCS carousel preview branches', () => {
77
+ const buildRcsContent = (cards) => ({
78
+ rcsPreviewContent: {
79
+ carouselData: cards,
80
+ },
81
+ });
82
+
83
+ const getNodesByClassName = (className) =>
84
+ renderedComponent.findWhere((node) => node.prop('className') === className);
85
+
86
+ it('renders image and video cards with proper media-specific markup', () => {
87
+ renderFunction(
88
+ RCS,
89
+ buildRcsContent([
90
+ {
91
+ mediaType: 'image',
92
+ imageSrc: 'https://example.com/image-card.jpg',
93
+ bodyText: 'image body',
94
+ suggestions: [],
95
+ },
96
+ {
97
+ mediaType: 'video',
98
+ videoPreviewImg: 'https://example.com/video-thumb.jpg',
99
+ bodyText: 'video body',
100
+ suggestions: [],
101
+ },
102
+ ]),
103
+ '',
104
+ 0,
105
+ );
106
+
107
+ // Video card should render tooltip/video icon path; image-only card should not.
108
+ expect(renderedComponent.find('CapTooltip')).toHaveLength(1);
109
+ expect(
110
+ renderedComponent.findWhere((node) => node.prop('className') === 'video-icon')
111
+ ).toHaveLength(1);
112
+
113
+ // Both cards render media containers.
114
+ expect(
115
+ renderedComponent.findWhere((node) => node.prop('className') === 'whatsapp-image')
116
+ ).toHaveLength(2);
117
+ });
118
+
119
+ it('renders per-card suggestions only for cards that provide suggestions', () => {
120
+ renderFunction(
121
+ RCS,
122
+ buildRcsContent([
123
+ {
124
+ mediaType: 'image',
125
+ imageSrc: 'https://example.com/with-suggestions.jpg',
126
+ bodyText: 'has suggestions',
127
+ suggestions: [
128
+ { type: 'QUICK_REPLY', text: 'Reply 1' },
129
+ { type: 'CTA', text: 'Visit' },
130
+ ],
131
+ },
132
+ {
133
+ mediaType: 'image',
134
+ imageSrc: 'https://example.com/no-suggestions.jpg',
135
+ bodyText: 'no suggestions',
136
+ suggestions: [],
137
+ },
138
+ ]),
139
+ '',
140
+ 0,
141
+ );
142
+
143
+ // Suggestion labels/icons should be rendered only from first card.
144
+ expect(getNodesByClassName('rcs-cta-preview')).toHaveLength(2);
145
+ expect(getNodesByClassName('whatsapp-divider')).not.toHaveLength(0);
146
+ });
147
+
148
+ it('falls back to empty preview assets when image/video media src is missing', () => {
149
+ renderFunction(
150
+ RCS,
151
+ buildRcsContent([
152
+ {
153
+ mediaType: 'image',
154
+ bodyText: 'fallback image',
155
+ suggestions: [],
156
+ },
157
+ {
158
+ mediaType: 'video',
159
+ bodyText: 'fallback video',
160
+ suggestions: [],
161
+ },
162
+ ]),
163
+ '',
164
+ 0,
165
+ );
166
+
167
+ const mediaImageNodes = renderedComponent.findWhere(
168
+ (node) => node.name() === 'CapImage' && node.prop('className') === 'whatsapp-image'
169
+ );
170
+ const mediaSrcs = mediaImageNodes.map((node) => node.prop('src'));
171
+
172
+ expect(mediaSrcs).toContain(whatsappImageEmptyPreview);
173
+ expect(mediaSrcs).toContain(whatsappVideoEmptyPreview);
174
+ });
175
+
176
+ it('generates unique carousel keys using rcs-carousel-<idx>-<bodyText|imageSrc|videoPreviewImg>', () => {
177
+ renderFunction(
178
+ RCS,
179
+ buildRcsContent([
180
+ {
181
+ mediaType: 'image',
182
+ imageSrc: 'https://example.com/one.jpg',
183
+ bodyText: 'same body',
184
+ suggestions: [],
185
+ },
186
+ {
187
+ mediaType: 'image',
188
+ imageSrc: 'https://example.com/two.jpg',
189
+ bodyText: 'same body',
190
+ suggestions: [],
191
+ },
192
+ {
193
+ mediaType: 'video',
194
+ videoPreviewImg: 'https://example.com/three-thumb.jpg',
195
+ bodyText: 'same body',
196
+ suggestions: [],
197
+ },
198
+ ]),
199
+ '',
200
+ 0,
201
+ );
202
+
203
+ const cardNodes = getNodesByClassName('message-pop align-left message-pop-carousel rcs-carousel-card');
204
+ const keys = cardNodes.map((node) => node.key());
205
+
206
+ expect(keys).toEqual([
207
+ 'rcs-carousel-0-same body',
208
+ 'rcs-carousel-1-same body',
209
+ 'rcs-carousel-2-same body',
210
+ ]);
211
+ expect(new Set(keys).size).toBe(keys.length);
212
+ });
213
+ });
72
214
  });
@@ -18,7 +18,7 @@ import injectReducer from '../../utils/injectReducer';
18
18
  import injectSaga from '../../utils/injectSaga';
19
19
 
20
20
  import CommonTestAndPreview from '../CommonTestAndPreview';
21
- import { CHANNELS } from '../CommonTestAndPreview/constants';
21
+ import { CHANNELS, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../CommonTestAndPreview/constants';
22
22
  import * as commonTestAndPreviewActions from '../CommonTestAndPreview/actions';
23
23
  import { commonTestAndPreviewSaga } from '../CommonTestAndPreview/sagas';
24
24
  import commonTestAndPreviewReducer from '../CommonTestAndPreview/reducer';
@@ -78,6 +78,16 @@ TestAndPreviewSlidebox.propTypes = {
78
78
  content: PropTypes.string,
79
79
  beeInstance: PropTypes.object,
80
80
  currentTab: PropTypes.number,
81
+ smsFallbackContent: PropTypes.shape({
82
+ templateContent: PropTypes.string,
83
+ senderId: PropTypes.string,
84
+ templateName: PropTypes.string,
85
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: PropTypes.object,
86
+ }),
87
+ /** Passed to CommonTestAndPreview for RCS test-meta resolution (slot semantics vs full-mode). */
88
+ rcsTestPreviewOptions: PropTypes.shape({
89
+ isLibraryMode: PropTypes.bool,
90
+ }),
81
91
  // Redux props are passed through
82
92
  actions: PropTypes.object.isRequired,
83
93
  extractedTags: PropTypes.array.isRequired,
@@ -109,10 +119,12 @@ TestAndPreviewSlidebox.defaultProps = {
109
119
  currentTab: 1,
110
120
  messageMetaConfigId: null,
111
121
  prefilledValues: {},
122
+ rcsTestPreviewOptions: undefined,
112
123
  senderDetailsByChannel: {},
113
124
  wecrmAccounts: [],
114
125
  isLoadingSenderDetails: false,
115
126
  orgUnitId: -1,
127
+ smsFallbackContent: null,
116
128
  };
117
129
 
118
130
  const mapStateToProps = createStructuredSelector({