@capillarytech/creatives-library 8.0.353-alpha.5 → 8.0.353-alpha.6

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 (136) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapTagList/index.js +10 -0
  15. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +213 -21
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  22. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +0 -17
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -146
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +138 -48
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -4
  29. package/v2Components/CommonTestAndPreview/index.js +691 -235
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +0 -159
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -256
  42. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -2
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -198
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +36 -26
  46. package/v2Components/FormBuilder/index.js +11 -6
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +956 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +38 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -31
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +15 -3
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/App/constants.js +0 -3
  71. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  72. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  73. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  74. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  75. package/v2Containers/CreativesContainer/constants.js +9 -0
  76. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  77. package/v2Containers/CreativesContainer/index.js +322 -103
  78. package/v2Containers/CreativesContainer/index.scss +51 -1
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  88. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  89. package/v2Containers/Rcs/constants.js +119 -10
  90. package/v2Containers/Rcs/index.js +2445 -813
  91. package/v2Containers/Rcs/index.scss +280 -8
  92. package/v2Containers/Rcs/messages.js +34 -3
  93. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  94. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  95. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  96. package/v2Containers/Rcs/tests/index.test.js +152 -121
  97. package/v2Containers/Rcs/tests/mockData.js +38 -0
  98. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  99. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  100. package/v2Containers/Rcs/utils.js +478 -11
  101. package/v2Containers/Sms/Create/index.js +106 -40
  102. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  103. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  104. package/v2Containers/SmsTrai/Create/index.js +9 -4
  105. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  106. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  107. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  108. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  109. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  110. package/v2Containers/SmsWrapper/index.js +37 -8
  111. package/v2Containers/TagList/index.js +6 -0
  112. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  113. package/v2Containers/Templates/_templates.scss +166 -9
  114. package/v2Containers/Templates/actions.js +11 -0
  115. package/v2Containers/Templates/constants.js +2 -0
  116. package/v2Containers/Templates/index.js +122 -120
  117. package/v2Containers/Templates/sagas.js +56 -12
  118. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  119. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  120. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  121. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  122. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  123. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  124. package/v2Containers/TemplatesV2/index.js +86 -23
  125. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  126. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  127. package/v2Containers/WebPush/Create/index.js +8 -91
  128. package/v2Containers/WebPush/Create/index.scss +0 -7
  129. package/v2Containers/Whatsapp/index.js +3 -20
  130. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  131. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +0 -169
  132. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +0 -522
  133. package/v2Containers/App/tests/constants.test.js +0 -61
  134. package/v2Containers/Templates/tests/webpush.test.js +0 -375
  135. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +0 -338
  136. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +0 -325
@@ -52,6 +52,7 @@ import { QUICK_REPLY, WHATSAPP_CATEGORIES, PHONE_NUMBER, TEMPLATE_VARIABLE_REGEX
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');
@@ -236,7 +237,23 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
236
237
  let content = channel && channel.toLowerCase() === 'sms' ? [this.props.content] : this.props.content;
237
238
  const { formatMessage } = intl;
238
239
  const { rcsPreviewContent, inAppPreviewContent, viberPreviewContent, isBeeFreeTemplate } = content || {};
239
- const { rcsImageSrc, rcsVideoSrc, rcsTitle, rcsDesc, rcsSuggestions } = rcsPreviewContent || {};
240
+ const normalizedRcsPreviewContent = Array.isArray(rcsPreviewContent)
241
+ ? { carouselData: rcsPreviewContent }
242
+ : (rcsPreviewContent || {});
243
+ const {
244
+ rcsImageSrc,
245
+ rcsVideoSrc,
246
+ rcsTitle,
247
+ rcsDesc,
248
+ rcsSuggestions,
249
+ carouselData: rcsCarouselData,
250
+ rcsCarouselData: rcsCarouselDataAlt,
251
+ cardVarMapped: rcsCardVarMapped,
252
+ } = normalizedRcsPreviewContent;
253
+ const resolvedRcsCarouselData =
254
+ Array.isArray(rcsCarouselData) && rcsCarouselData.length > 0
255
+ ? rcsCarouselData
256
+ : rcsCarouselDataAlt;
240
257
  const {
241
258
  videoParams,
242
259
  imageURL,
@@ -317,6 +334,18 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
317
334
  'flex-shrink': 0,
318
335
  'left': 0,
319
336
  };
337
+
338
+ const resolveVarsWithMap = (input, varMap) => {
339
+ if (input === null || input === undefined) return '';
340
+ const str = typeof input === 'string' ? input : String(input);
341
+ return str.replace(TEMPLATE_VAR_REGEX, (token, varName) => {
342
+ const mappedValue = varMap?.[varName];
343
+ if (mappedValue === null || mappedValue === undefined || String(mappedValue) === '') {
344
+ return token;
345
+ }
346
+ return String(mappedValue);
347
+ });
348
+ };
320
349
  const getVideoContent = ({
321
350
  video,
322
351
  actionUrl,
@@ -473,13 +502,15 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
473
502
  const whatsappUpdatedAccountName = whatsappAccountName || templateData?.versions?.base?.content?.whatsapp?.accountName || '';
474
503
  const whatsappUpdatedLen = whatsappContentLen !== undefined ? whatsappContentLen : content?.charCount;
475
504
 
476
- const renderRcsSuggestionsPreview = () => {
505
+ const renderRcsSuggestionsPreview = (suggestionsArg) => {
477
506
  const renderArray = [];
478
- (rcsSuggestions || []).forEach((suggestion) => {
479
- renderArray.push(<CapDivider className="whatsapp-divider" />);
507
+ const suggestions = Array.isArray(suggestionsArg) ? suggestionsArg : (rcsSuggestions || []);
508
+ (suggestions || []).forEach((suggestion, idx) => {
509
+ const suggestionKey = `${suggestion?.type || 'unknown'}-${suggestion?.text || ''}-${idx}`;
510
+ renderArray.push(<CapDivider key={`rcs-divider-${suggestionKey}`} className="whatsapp-divider" />);
480
511
  if (suggestion.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
481
512
  renderArray.push(
482
- <CapLabel type="label21" className="rcs-cta-preview">
513
+ <CapLabel key={`rcs-suggestion-${suggestionKey}`} type="label21" className="rcs-cta-preview">
483
514
  <CapIcon
484
515
  type='small-link'
485
516
  size="xs"
@@ -489,14 +520,14 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
489
520
  );
490
521
  } else if (suggestion.type === RCS_BUTTON_TYPES.CTA) {
491
522
  renderArray.push(
492
- <CapLabel type="label21" className="rcs-cta-preview">
523
+ <CapLabel key={`rcs-suggestion-${suggestionKey}`} type="label21" className="rcs-cta-preview">
493
524
  <CapIcon type="launch" size="xs" />
494
525
  {suggestion.text}
495
526
  </CapLabel>,
496
527
  );
497
528
  } else if (suggestion.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
498
529
  renderArray.push(
499
- <CapLabel type="label21" className="rcs-cta-preview">
530
+ <CapLabel key={`rcs-suggestion-${suggestionKey}`} type="label21" className="rcs-cta-preview">
500
531
  <CapIcon type="call" size="xs" />
501
532
  {suggestion.text}
502
533
  </CapLabel>,
@@ -509,23 +540,20 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
509
540
  const renderTextPreviewContent = () => (
510
541
  <>
511
542
  {rcsTitle && (
512
- <>
513
543
  <CapLabel
514
544
  type="label5"
515
545
  className="message-pop-item align-left rcs-content"
516
546
  fontWeight="bold"
517
547
  >
518
- {rcsTitle}
548
+ {resolveVarsWithMap(rcsTitle, rcsCardVarMapped)}
519
549
  </CapLabel>
520
- <CapDivider className="whatsapp-divider" />
521
- </>
522
550
  )}
523
551
  {rcsDesc && (
524
552
  <CapLabel
525
553
  type="label5"
526
554
  className="message-pop-item align-left rcs-desc rcs-content"
527
555
  >
528
- {rcsDesc}
556
+ {resolveVarsWithMap(rcsDesc, rcsCardVarMapped)}
529
557
  </CapLabel>
530
558
  )}
531
559
  {renderRcsSuggestionsPreview()}
@@ -542,50 +570,134 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
542
570
  />
543
571
  )}
544
572
  {rcsVideoSrc && (
545
- <div className="video-preview">
573
+ <CapRow className="video-preview">
546
574
  <CapImage
547
575
  src={rcsVideoSrc}
548
576
  className="rcs-image"
549
577
  alt={formatMessage(messages.previewGenerated)}
550
578
  />
551
- <div className="icon-position">
579
+ <CapRow className="icon-position">
552
580
  <CapImage
553
581
  className="video-icon"
554
582
  src={videoPlay}
555
583
  />
556
- </div>
557
- </div>
584
+ </CapRow>
585
+ </CapRow>
558
586
  )}
559
587
  </>
560
588
  );
561
589
 
562
590
  const renderRcsPreviewContent = () => {
591
+ const carouselCards = Array.isArray(resolvedRcsCarouselData) ? resolvedRcsCarouselData : [];
592
+ if (carouselCards.length > 0) {
593
+ return (
594
+ <CapRow className="msg-container sms">
595
+ <CapRow className="message-pop sms">
596
+ <CapRow className="msg-container-carousel">
597
+ <CapRow className="scroll-container">
598
+ {carouselCards.map((card, idx) => {
599
+ const key = `rcs-carousel-${idx}-${card?.bodyText || card?.imageSrc || card?.videoPreviewImg || ''}`;
600
+ const isVideo = (card?.mediaType || '').toLowerCase() === 'video';
601
+ const cardSuggestions = Array.isArray(card?.suggestions) ? card.suggestions : [];
602
+ const effectiveCardVarMap = card?.cardVarMapped || rcsCardVarMapped;
603
+ const suggestionsNode = cardSuggestions.length > 0
604
+ ? renderRcsSuggestionsPreview(cardSuggestions)
605
+ : null;
606
+
607
+ const resolvedCardTitle = resolveVarsWithMap(card?.title, effectiveCardVarMap);
608
+ const resolvedCardBodyText = resolveVarsWithMap(card?.bodyText, effectiveCardVarMap);
609
+
610
+ return (
611
+ <CapRow
612
+ key={key}
613
+ className="message-pop align-left message-pop-carousel rcs-carousel-card"
614
+ >
615
+ <CapRow className="whatsapp-content">
616
+ {!isVideo && (
617
+ <CapImage
618
+ src={card?.imageSrc ? card.imageSrc : whatsappImageEmptyPreview}
619
+ className="whatsapp-image"
620
+ alt={formatMessage(messages.previewGenerated)}
621
+ />
622
+ )}
623
+ {isVideo && (
624
+ <CapTooltip title={formatMessage(messages.videoPreviewTooltip)}>
625
+ <CapRow className="video-preview">
626
+ <CapImage
627
+ src={card?.videoPreviewImg ? card.videoPreviewImg : whatsappVideoEmptyPreview}
628
+ className="whatsapp-image"
629
+ alt={formatMessage(messages.previewGenerated)}
630
+ />
631
+ <CapRow className="icon-position">
632
+ <CapImage className="video-icon" src={videoPlay} alt="Play" />
633
+ </CapRow>
634
+ </CapRow>
635
+ </CapTooltip>
636
+ )}
637
+
638
+ {(resolvedCardTitle || resolvedCardBodyText) && (
639
+ <CapRow className="carousel-content">
640
+ {!!resolvedCardTitle && (
641
+ <CapLabel
642
+ type={card?.titleLabelType || 'label1'}
643
+ className="carousel-title"
644
+ >
645
+ {resolvedCardTitle}
646
+ </CapLabel>
647
+ )}
648
+ {!!resolvedCardBodyText && (
649
+ <CapLabel
650
+ type={card?.bodyLabelType || 'label2'}
651
+ className="carousel-message"
652
+ >
653
+ {resolvedCardBodyText}
654
+ </CapLabel>
655
+ )}
656
+ </CapRow>
657
+ )}
658
+
659
+ {!!suggestionsNode && (
660
+ <>
661
+ {suggestionsNode}
662
+ </>
663
+ )}
664
+ </CapRow>
665
+ </CapRow>
666
+ );
667
+ })}
668
+ </CapRow>
669
+ </CapRow>
670
+ </CapRow>
671
+ </CapRow>
672
+ );
673
+ }
674
+
563
675
  if (rcsOrientation === HORIZONTAL) {
564
676
  return rcsType === RIGHT ? (
565
- <div className="msg-container sms">
566
- <div className="message-pop sms horizontal">
677
+ <CapRow className="msg-container sms">
678
+ <CapRow className="message-pop sms horizontal">
567
679
  <CapColumn className="rcs-preview-text" span={12}>{renderTextPreviewContent()}</CapColumn>
568
680
  <CapColumn span={12}>{renderMediaPreviewContent()}</CapColumn>
569
- </div>
570
- </div>
681
+ </CapRow>
682
+ </CapRow>
571
683
 
572
684
  ) : (
573
- <div className="msg-container sms">
574
- <div className="message-pop sms horizontal">
685
+ <CapRow className="msg-container sms">
686
+ <CapRow className="message-pop sms horizontal">
575
687
  <CapColumn span={12}>{renderMediaPreviewContent()}</CapColumn>
576
688
  <CapColumn className="rcs-preview-text" span={12}>{renderTextPreviewContent()}</CapColumn>
577
- </div>
578
- </div>
689
+ </CapRow>
690
+ </CapRow>
579
691
  );
580
692
  }
581
693
 
582
694
  return (
583
- <div className="msg-container sms">
584
- <div className="message-pop sms">
695
+ <CapRow className="msg-container sms">
696
+ <CapRow className="message-pop sms">
585
697
  {renderMediaPreviewContent()}
586
698
  {renderTextPreviewContent()}
587
- </div>
588
- </div>
699
+ </CapRow>
700
+ </CapRow>
589
701
  );
590
702
  };
591
703
 
@@ -1303,14 +1415,14 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
1303
1415
  ""
1304
1416
  )}
1305
1417
  {channel?.toUpperCase() === RCS && (
1306
- <div className="shell-v2 align-center rcs-preview">
1418
+ <CapRow className="shell-v2 align-center rcs-preview">
1307
1419
  <CapImage
1308
1420
  className="preview-image"
1309
1421
  src={rcsIosPreview ? smsMobileIos : smsMobileAndroid}
1310
1422
  alt={formatMessage(messages.previewGenerated)}
1311
1423
  />
1312
1424
  {renderRcsPreviewContent()}
1313
- </div>
1425
+ </CapRow>
1314
1426
 
1315
1427
  )}
1316
1428
  {channel?.toUpperCase() === ZALO && (
@@ -1511,4 +1623,4 @@ TemplatePreview.propTypes = {
1511
1623
  rcsOrientation: PropTypes.string,
1512
1624
  };
1513
1625
 
1514
- export default (injectIntl(TemplatePreview));
1626
+ 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';
@@ -69,8 +69,8 @@ const TestAndPreviewSlidebox = (props) => {
69
69
 
70
70
  TestAndPreviewSlidebox.propTypes = {
71
71
  // Channel prop - supports all channels
72
- channel: PropTypes.oneOf([CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.INAPP, CHANNELS.MOBILEPUSH, CHANNELS.VIBER, CHANNELS.ZALO, CHANNELS.WEBPUSH]),
73
- currentChannel: PropTypes.oneOf([CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.INAPP, CHANNELS.MOBILEPUSH, CHANNELS.VIBER, CHANNELS.ZALO, CHANNELS.WEBPUSH]), // Alternative prop name for backward compatibility
72
+ channel: PropTypes.oneOf([CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.INAPP, CHANNELS.MOBILEPUSH]),
73
+ currentChannel: PropTypes.oneOf([CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.INAPP, CHANNELS.MOBILEPUSH]), // Alternative prop name for backward compatibility
74
74
  // All original props are passed through
75
75
  show: PropTypes.bool.isRequired,
76
76
  onClose: PropTypes.func.isRequired,
@@ -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({
@@ -34,6 +34,7 @@ import {
34
34
  GET_PREFILLED_VALUES_SUCCESS,
35
35
  GET_PREFILLED_VALUES_FAILURE,
36
36
  } from './constants';
37
+ import { extractPreviewFromLiquidResponse } from '../CommonTestAndPreview/previewApiUtils';
37
38
 
38
39
  // Search Customers Saga
39
40
  export function* searchCustomersSaga(action) {
@@ -80,11 +81,12 @@ export function* updatePreviewSaga(action) {
80
81
  const customValues = action.payload.resolvedTags;
81
82
 
82
83
  const response = yield call(Api.updateEmailPreview, action.payload);
83
- if (response?.data) {
84
+ const previewPayload = extractPreviewFromLiquidResponse(response);
85
+ if (previewPayload) {
84
86
  yield put({
85
87
  type: UPDATE_PREVIEW_SUCCESS,
86
88
  payload: {
87
- previewData: response.data,
89
+ previewData: previewPayload,
88
90
  customValues, // Pass custom values to be preserved
89
91
  },
90
92
  });
@@ -221,8 +223,13 @@ export function* createMessageMetaSaga(action) {
221
223
  export function* getPrefilledValuesSaga(action) {
222
224
  try {
223
225
  const response = yield call(Api.updateEmailPreview, action.payload);
224
- if (response?.data) {
225
- yield put({ type: GET_PREFILLED_VALUES_SUCCESS, payload: { values: response?.data?.resolvedTagValues } });
226
+ const body =
227
+ response?.data !== undefined && response?.data !== null
228
+ ? response.data
229
+ : response;
230
+ const resolvedTagValues = body?.resolvedTagValues;
231
+ if (resolvedTagValues != null) {
232
+ yield put({ type: GET_PREFILLED_VALUES_SUCCESS, payload: { values: resolvedTagValues } });
226
233
  } else {
227
234
  yield put({ type: GET_PREFILLED_VALUES_FAILURE, payload: { error: response.error || 'Failed to fetch prefilled values' } });
228
235
  }
@@ -136,7 +136,9 @@ describe('TestAndPreviewSlidebox Sagas', () => {
136
136
  describe('updatePreviewSaga', () => {
137
137
  it('should handle successful preview update', () => {
138
138
  const mockResponse = {
139
- data: 'Test Preview Data',
139
+ data: {
140
+ resolvedBody: 'Test Preview Data',
141
+ },
140
142
  };
141
143
  const customValues = { test: 'value' };
142
144
  return expectSaga(sagas.updatePreviewSaga, {
@@ -0,0 +1,2 @@
1
+ /** Default prefix before variable name in variable-slot placeholders. */
2
+ export const VAR_SEGMENT_PLACEHOLDER_PREFIX = 'enter the value for ';
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared message editor that renders template text with {{var}} and/or DLT `{#var#}` segments as
3
+ * variable inputs and static text as headings.
4
+ * Reused by RCS (title/description), SmsTrai Edit (SMS fallback), and WhatsApp (edit message/header).
5
+ */
6
+ import React from 'react';
7
+ import PropTypes from 'prop-types';
8
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
9
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
10
+ import CapInput from '@capillarytech/cap-ui-library/CapInput';
11
+ import {
12
+ splitTemplateVarString,
13
+ DEFAULT_MUSTACHE_VAR_REGEX,
14
+ isAnyTemplateVarToken,
15
+ } from '../../utils/templateVarUtils';
16
+
17
+ import './index.scss';
18
+ import { VAR_SEGMENT_PLACEHOLDER_PREFIX } from './constants';
19
+
20
+ const { TextArea } = CapInput;
21
+
22
+ export function VarSegmentMessageEditor({
23
+ templateString = '',
24
+ valueMap = {},
25
+ onChange,
26
+ onFocus,
27
+ placeholderPrefix = VAR_SEGMENT_PLACEHOLDER_PREFIX,
28
+ getPlaceholder,
29
+ wrapperClassName = 'rcs_text_area_wrapper',
30
+ rowClassName = 'rcs-edit-template-message-input',
31
+ headingClassName = 'rcs-edit-template-message-split',
32
+ varRegex,
33
+ readOnly = false,
34
+ disabled = false,
35
+ footerContent,
36
+ renderVarFooter,
37
+ }) {
38
+ const segments = splitTemplateVarString(templateString, varRegex || DEFAULT_MUSTACHE_VAR_REGEX);
39
+ if (!segments?.length) return null;
40
+
41
+ return (
42
+ <div className={wrapperClassName}>
43
+ <CapRow className={rowClassName}>
44
+ {segments.map((segmentToken, segmentIndex) => {
45
+ const isVar =
46
+ typeof segmentToken === 'string' && isAnyTemplateVarToken(segmentToken);
47
+ if (isVar) {
48
+ const varSegmentFieldId = `${segmentToken}_${segmentIndex}`;
49
+ const slotValueFromMap = valueMap?.[varSegmentFieldId];
50
+ // Missing key: show empty (not the raw {{…}} token) so cleared slots and incomplete maps
51
+ // cannot resurrect the token; placeholder still guides the user.
52
+ const value =
53
+ slotValueFromMap !== undefined && slotValueFromMap !== null ? slotValueFromMap : '';
54
+ if (readOnly) {
55
+ return (
56
+ <CapHeading
57
+ key={varSegmentFieldId}
58
+ type="h4"
59
+ className={`${headingClassName} var-segment-message-editor__read-only-value`.trim()}
60
+ >
61
+ {value}
62
+ </CapHeading>
63
+ );
64
+ }
65
+ const fromGet = getPlaceholder && getPlaceholder(segmentToken, segmentIndex);
66
+ const placeholder =
67
+ fromGet !== undefined && fromGet !== null && fromGet !== ''
68
+ ? fromGet
69
+ : `${placeholderPrefix}${segmentToken}`;
70
+ return (
71
+ <div key={varSegmentFieldId} className="var-segment-message-editor__var-slot">
72
+ <TextArea
73
+ id={varSegmentFieldId}
74
+ placeholder={placeholder}
75
+ autosize={{ minRows: 1, maxRows: 3 }}
76
+ value={value}
77
+ onFocus={() => onFocus && onFocus(varSegmentFieldId)}
78
+ onChange={(e) =>
79
+ onChange && onChange(varSegmentFieldId, e?.target?.value ?? '')}
80
+ disabled={disabled}
81
+ />
82
+ {renderVarFooter
83
+ ? renderVarFooter(segmentToken, segmentIndex, varSegmentFieldId)
84
+ : null}
85
+ </div>
86
+ );
87
+ }
88
+ if (segmentToken) {
89
+ return (
90
+ <CapHeading
91
+ key={`static_${segmentIndex}_${segmentToken}`}
92
+ type="h4"
93
+ className={headingClassName}
94
+ >
95
+ {segmentToken}
96
+ </CapHeading>
97
+ );
98
+ }
99
+ return null;
100
+ })}
101
+ </CapRow>
102
+ {footerContent}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ VarSegmentMessageEditor.propTypes = {
108
+ templateString: PropTypes.string,
109
+ valueMap: PropTypes.object,
110
+ onChange: PropTypes.func,
111
+ onFocus: PropTypes.func,
112
+ placeholderPrefix: PropTypes.string,
113
+ getPlaceholder: PropTypes.func,
114
+ wrapperClassName: PropTypes.string,
115
+ rowClassName: PropTypes.string,
116
+ headingClassName: PropTypes.string,
117
+ varRegex: PropTypes.object,
118
+ readOnly: PropTypes.bool,
119
+ disabled: PropTypes.bool,
120
+ footerContent: PropTypes.node,
121
+ /** Optional hint below a variable field (e.g. DLT `{#var#}` max length). */
122
+ renderVarFooter: PropTypes.func,
123
+ };
124
+
125
+ export default VarSegmentMessageEditor;