PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 All 50 releases
← All changes | app/Services/Renderer/ProductRenderer.php +239 -92 1.5.2 → 1.6.5 View file →
@@ -13,8 +13,9 @@
13 13 use FluentCart\App\Http\Routes\WebRoutes;
14 14 use FluentCart\App\Models\ProductVariation;
15 15 use FluentCart\Framework\Support\Collection;
16 16 use FluentCart\App\Modules\Templating\AssetLoader;
17 +use FluentCart\App\Modules\FluentPlayer\ProductVideoRenderer;
17 18
18 19 class ProductRenderer
19 20 {
20 21 protected $product;
@@ -38,8 +39,10 @@
38 39 protected $defaultGalleryImageId = 0;
39 40
40 41 protected $galleryActiveSet = false;
41 42
43 + protected $productVideoRenderer = null;
44 +
42 45 protected $paymentTypes = [];
43 46
44 47 protected $variantsByPaymentTypes = [];
45 48
@@ -294,8 +297,19 @@
294 297 }
295 298
296 299 public function renderBuySection($atts = [])
297 300 {
301 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
302 +
303 + // The buy section is the root the variation-selector JS binds to
304 + // (data-fluent-cart-product-pricing-section). Gating it removes the
305 + // variation picker along with the buttons, which is what a full catalog
306 + // mode wants; to keep shoppers able to browse options while hiding only
307 + // the purchase affordances, gate 'actions' instead.
308 + if (!RenderGate::shouldRender('buy_section', $gateContext)) {
309 + return;
310 + }
311 +
298 312 // Render no buy section when there is nothing purchasable — avoids a
299 313 // broken quantity + "Not Available" block. Two cases:
300 314 // - no variants at all (any product type), or
301 315 // - an advanced-variation product with no attribute_config yet (e.g.
@@ -421,11 +435,16 @@
421 435 if (empty($this->defaultImageUrl)) {
422 436 $this->defaultImageUrl = $featuredMedia;
423 437 }
424 438
439 + $videoRenderer = $this->getProductVideoRenderer();
440 + $videoRenderer->showFirstByDefault(!$this->hasGalleryImages());
441 + $defaultVideoId = $videoRenderer->getDefaultMediaId();
442 +
425 443 ?>
426 - <div class="fct-product-gallery-thumb" role="region"
427 - aria-label="<?php echo esc_attr($this->product->post_title . ' gallery'); ?>">
444 + <div class="fct-product-gallery-thumb<?php echo $defaultVideoId ? ' is-video-active' : ''; ?>" role="region"
445 + aria-label="<?php echo esc_attr($this->product->post_title . ' gallery'); ?>"
446 + <?php echo $defaultVideoId ? 'data-fct-video-default="' . esc_attr((string) $defaultVideoId) . '"' : ''; ?>>
428 447 <img
429 448 src="<?php echo esc_url($this->defaultImageUrl ?? '') ?>"
430 449 alt="<?php echo esc_attr($this->defaultImageAlt) ?>"
431 450 data-fluent-cart-single-product-page-product-thumbnail
@@ -430,21 +449,54 @@
430 449 alt="<?php echo esc_attr($this->defaultImageAlt) ?>"
431 450 data-fluent-cart-single-product-page-product-thumbnail
432 451 data-default-image-url="<?php echo esc_url($featuredMedia) ?>"
433 452 />
453 + <?php $this->getProductVideoRenderer()->renderInlinePlayers(); ?>
434 454 </div>
435 455 <?php
436 456 }
437 457
458 + /**
459 + * Whether any gallery / variant image exists to show in the main area.
460 + * Only meaningful after renderGalleryThumb() has built $this->images.
461 + */
462 + protected function hasGalleryImages(): bool
463 + {
464 + foreach ($this->images as $image) {
465 + foreach ((array) Arr::get($image, 'media', []) as $item) {
466 + if (!empty(Arr::get($item, 'url'))) {
467 + return true;
468 + }
469 + }
470 + }
471 +
472 + return false;
473 + }
474 +
475 + public function getProductVideoRenderer(): ProductVideoRenderer
476 + {
477 + if ($this->productVideoRenderer === null) {
478 + $this->productVideoRenderer = new ProductVideoRenderer($this->product);
479 + }
480 +
481 + return $this->productVideoRenderer;
482 + }
483 +
438 484 public function renderGalleryThumbControls($maxThumbnails = null)
439 485 {
440 486 $totalThumbImages = Arr::pluck($this->images, 'media.*.url');
441 487
442 - if(count($totalThumbImages) == 1 && is_countable($totalThumbImages[0]) && count($totalThumbImages[0]) == 1){
488 + // A lone image needs no strip, but a video thumb still has to be reachable.
489 + if(count($totalThumbImages) == 1 && is_countable($totalThumbImages[0]) && count($totalThumbImages[0]) == 1 && !$this->getProductVideoRenderer()->isAvailable()){
443 490
444 491 return '';
445 492 }
446 493
494 + // A lone video without images is already showing in the main area.
495 + if (!$this->hasGalleryImages() && count($this->getProductVideoRenderer()->getVideos()) === 1) {
496 + return '';
497 + }
498 +
447 499 // Build variantTermMap and variantFirstMediaMap.
448 500 // For advanced variations, Pro builds both maps via the gallery_variation_data
449 501 // filter — it has access to AttributeGroup types (color/image) that free cannot
450 502 // query directly. For all other product types, free builds variantFirstMediaMap
@@ -537,9 +589,16 @@
537 589 $isAdvVariation = !empty($this->variantTermMap);
538 590 $countedMediaKeys = [];
539 591 $count = 0;
540 592 $totalImages = 0;
593 + $videoRenderer = $this->getProductVideoRenderer();
541 594
595 + // The gallery opens on a video the admin put in front of every image,
596 + // so no image thumb takes the selected slot in that case.
597 + if ($videoRenderer->getDefaultMediaId()) {
598 + $this->galleryActiveSet = true;
599 + }
600 +
542 601 // Count unique images to render. For advanced variations, deduplicate by WP media ID
543 602 // when available, or by URL for externally imported images (media ID = 0).
544 603 foreach ($this->images as $imageId => $image) {
545 604 if (empty($image['media']) || !is_array($image['media'])) {
@@ -581,16 +640,21 @@
581 640 }
582 641 $renderedMediaKeys[$mediaDedupeKey] = true;
583 642 }
584 643 if ($maxThumbnails !== null && $count >= (int) $maxThumbnails) {
644 + $videoRenderer->renderThumbControls(!$this->galleryActiveSet);
585 645 $this->renderGallerySeeMoreButton($totalImages - (int) $maxThumbnails);
586 646 return;
587 647 }
648 + // Videos the admin dragged in front of this image come first.
649 + $videoRenderer->renderThumbControlsBefore($count, !$this->galleryActiveSet);
588 650 $termId = (int) ($this->variantTermMap[(int) $imageId] ?? 0);
589 651 $this->renderGalleryThumbControlButton($item, $imageId, $termId, $mediaId);
590 652 $count++;
591 653 }
592 654 }
655 +
656 + $videoRenderer->renderThumbControls(!$this->galleryActiveSet);
593 657 }
594 658
595 659 public function renderGallerySeeMoreButton($remainingCount)
596 660 {
@@ -663,9 +727,14 @@
663 727 }
664 728
665 729 public function renderGallery($args = [])
666 730 {
731 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
667 732
733 + if (!RenderGate::shouldRender('image', $gateContext)) {
734 + return;
735 + }
736 +
668 737 $defaults = [
669 738 'thumbnail_mode' => 'all', // horizontal, vertical
670 739 'thumb_position' => 'bottom', // bottom, left, right, top
671 740 'scrollable_thumbs' => 'no', // yes / no
@@ -699,13 +768,21 @@
699 768 }
700 769
701 770 public function renderTitle()
702 771 {
772 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
773 +
774 + if (!RenderGate::shouldRender('title', $gateContext)) {
775 + return;
776 + }
777 +
778 + do_action('fluent_cart/product/single/before_title_block', $gateContext);
703 779 ?>
704 780 <div class="fct-product-title">
705 781 <h1 id="fct-product-summary-title"><?php echo esc_html($this->product->post_title); ?></h1>
706 782 </div>
707 783 <?php
784 + do_action('fluent_cart/product/single/after_title_block', $gateContext);
708 785 }
709 786
710 787 public function renderStockAvailability($wrapper_attributes = '')
711 788 {
@@ -725,15 +802,44 @@
725 802 // Check default variant stock for both simple and variable products
726 803 if ($this->defaultVariant) {
727 804 $isStock = $isStock && $this->defaultVariant->isStock();
728 805 }
806 +
729 807 $stockLabel = Arr::get($stockAvailability, 'availability');
730 - $statusClass = $isStock ? ($stockAvailability['class'] ?? '') : 'out-of-stock';
808 + $statusClass = $stockAvailability['class'] ?? '';
809 +
810 + // Optional per-status custom labels (e.g. set on the Bricks Product Stock
811 + // element via the fluent_cart/product_stock_availability filter). Emitted as
812 + // data-attributes so the frontend JS, which re-derives the badge text on load
813 + // and on variant switches, prefers them over the generic label map instead of
814 + // overwriting them. Absent for the default template, so behavior is unchanged.
815 + $inStockText = Arr::get($stockAvailability, 'in_stock_text');
816 + $outOfStockText = Arr::get($stockAvailability, 'out_of_stock_text');
817 +
818 + // The variant-level check above can override the aggregate stock_availability
819 + // used for $stockLabel/$statusClass (e.g. this specific default variant is out
820 + // of stock even though the product overall has other in-stock variants) — keep
821 + // the label and class in sync so the badge never shows mismatched text/color.
822 + // Honor the custom out-of-stock label here too so it survives this override on
823 + // first load, matching what the frontend JS shows after a variant switch.
824 + if (!$isStock) {
825 + $statusClass = 'out-of-stock';
826 + $stockLabel = !empty($outOfStockText) ? $outOfStockText : __('Out of Stock', 'fluent-cart');
827 + }
828 +
829 + $badgeAttributes = '';
830 + if (!empty($inStockText)) {
831 + $badgeAttributes .= sprintf(' data-in-stock-text="%s"', esc_attr($inStockText));
832 + }
833 + if (!empty($outOfStockText)) {
834 + $badgeAttributes .= sprintf(' data-out-of-stock-text="%s"', esc_attr($outOfStockText));
835 + }
836 +
731 837 echo sprintf(
732 838 '<div class="fct-product-stock %1$s" role="status" aria-live="polite">
733 839 <div %2$s>
734 840 <span class="fct-stock-label">%3$s</span>
735 - <span class="fct-stock-badge fct_status_badge_%1$s" data-fluent-cart-product-stock>
841 + <span class="fct-stock-badge fct_status_badge_%1$s" data-fluent-cart-product-stock%5$s>
736 842 %4$s
737 843 </span>
738 844 </div>
739 845 </div>',
@@ -739,9 +845,10 @@
739 845 </div>',
740 846 esc_attr($statusClass),
741 847 $wrapper_attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
742 848 esc_html__('Availability:', 'fluent-cart'),
743 - esc_html($stockLabel)
849 + esc_html($stockLabel),
850 + $badgeAttributes // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- values passed through esc_attr() above
744 851 );
745 852 }
746 853
747 854 public function renderSku($wrapper_attributes = '', $showLabel = true, $label = '', $variant = null)
@@ -791,23 +898,24 @@
791 898 );
792 899 }
793 900 }
794 901
902 + /**
903 + * @deprecated Use PackageDescriptionRenderer::renderPackageDescription() directly.
904 + * Kept as a compatibility shim for external callers (themes, extensions) —
905 + * package-description rendering now lives in PackageDescriptionRenderer.
906 + */
795 907 public function renderPackageDescription($wrapper_attributes = '', $showName = true, $showDimensions = true, $showProductWeight = true, $showTotalWeight = true, $variant = null)
796 908 {
797 - if ($variant) {
798 - if (!$wrapper_attributes) {
799 - $wrapper_attributes = 'class="fct-package-description" data-fluent-cart-package-description';
800 - }
801 - (new ProductCardRender($this->product))->renderPackageDescription($wrapper_attributes, $showName, $showDimensions, $showProductWeight, $showTotalWeight, $variant);
802 - return;
803 - }
804 -
805 - foreach ($this->product->variants as $v) {
806 - $isHidden = ($this->defaultVariant && $this->defaultVariant->id != $v->id) ? ' is-hidden' : '';
807 - $variantWrapper = 'class="fct-package-description fluent-cart-product-variation-content' . $isHidden . '" data-fluent-cart-package-description data-variation-id="' . esc_attr($v->id) . '"';
808 - (new ProductCardRender($this->product))->renderPackageDescription($variantWrapper, $showName, $showDimensions, $showProductWeight, $showTotalWeight, $v);
809 - }
909 + (new PackageDescriptionRenderer($this->product))->renderPackageDescription(
910 + $wrapper_attributes,
911 + $showName,
912 + $showDimensions,
913 + $showProductWeight,
914 + $showTotalWeight,
915 + $variant,
916 + $this->defaultVariant
917 + );
810 918 }
811 919
812 920 /**
813 921 * Build a JSON string of package info for a variant (used as data attribute for JS switching).
@@ -876,14 +984,22 @@
876 984 $excerpt = $this->product->post_excerpt;
877 985 if (!$excerpt) {
878 986 return;
879 987 }
988 +
989 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
990 +
991 + if (!RenderGate::shouldRender('excerpt', $gateContext)) {
992 + return;
993 + }
994 +
995 + do_action('fluent_cart/product/single/before_excerpt_block', $gateContext);
880 996 ?>
881 997 <div class="fct-product-excerpt" aria-labelledby="fct-product-summary-title">
882 998 <p><?php echo wp_kses_post($excerpt); ?></p>
883 999 </div>
884 1000 <?php
885 -
1001 + do_action('fluent_cart/product/single/after_excerpt_block', $gateContext);
886 1002 }
887 1003
888 1004 public function renderDescription()
889 1005 {
@@ -913,8 +1029,14 @@
913 1029 }
914 1030
915 1031 public function renderPrices()
916 1032 {
1033 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1034 +
1035 + if (!RenderGate::shouldRender('price', $gateContext)) {
1036 + return;
1037 + }
1038 +
917 1039 if ($this->product->detail->variation_type === 'simple') {
918 1040 // we have to render for the simple product
919 1041
920 1042 $first_price = $this->product->variants()->first();
@@ -928,97 +1050,76 @@
928 1050 $comparePrice = $first_price ? (int)$first_price->compare_price : 0;
929 1051 if ($comparePrice <= $itemPrice) {
930 1052 $comparePrice = 0;
931 1053 }
932 - do_action('fluent_cart/product/single/before_price_block', [
1054 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
933 1055 'product' => $this->product,
934 1056 'current_price' => $itemPrice,
935 1057 'scope' => 'price_range'
936 - ]);
1058 + ]));
937 1059 ?>
938 - <?php
1060 + <div class="fct-price-range fct-product-prices">
939 1061
940 - if ($comparePrice) {
941 - $aria_label = sprintf(
942 - /* translators: 1: Original price, 2: Current item price */
943 - __('Original Price: %1$s, Price: %2$s', 'fluent-cart'),
944 - Helper::toDecimal($comparePrice),
945 - Helper::toDecimal($itemPrice)
946 - );
947 - } else {
948 - $aria_label = sprintf(
949 - /* translators: 1: Current item price */
950 - __('Price: %1$s', 'fluent-cart'),
951 - Helper::toDecimal($itemPrice)
952 - );
953 - }
954 -
955 - ?>
956 - <div class="fct-price-range fct-product-prices" role="term"
957 - aria-label="<?php echo esc_attr($aria_label); ?>">
958 -
959 1062 <?php if ($comparePrice): ?>
960 1063 <span class="fct-compare-price">
961 - <del aria-label="<?php echo esc_attr(__('Original price', 'fluent-cart')); ?>"><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></del>
1064 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1065 + <del><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></del>
962 1066 </span>
963 1067 <?php endif; ?>
964 - <span class="fct-item-price" aria-label="<?php echo esc_attr(__('Current price', 'fluent-cart')); ?>">
1068 + <span class="fct-item-price">
1069 + <span class="fct-sr-only"><?php echo $comparePrice ? esc_html__('Sale price:', 'fluent-cart') : esc_html__('Price:', 'fluent-cart'); ?></span>
965 1070 <?php echo esc_html(Helper::toDecimal($itemPrice)); ?>
966 - <?php do_action('fluent_cart/product/after_price', [
1071 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
967 1072 'product' => $this->product,
1073 + 'variant' => $first_price,
968 1074 'current_price' => $itemPrice,
969 1075 'scope' => 'price_range'
970 - ]); ?>
1076 + ])); ?>
1077 + <?php RenderHelper::renderPriceSuffix($this->product, $first_price, 'price_range'); ?>
971 1078 </span>
972 1079 </div>
973 1080 <?php
974 - do_action('fluent_cart/product/single/after_price_block', [
1081 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
975 1082 'product' => $this->product,
976 1083 'current_price' => $itemPrice,
977 1084 'scope' => 'price_range'
978 - ]);
1085 + ]));
979 1086 return;
980 1087 }
981 1088 $min_price = $this->product->detail->min_price;
982 1089 $max_price = $this->product->detail->max_price;
983 1090
984 - do_action('fluent_cart/product/single/before_price_range_block', [
1091 + do_action('fluent_cart/product/single/before_price_range_block', RenderContext::decorate([
985 1092 'product' => $this->product,
986 1093 'current_price' => $min_price,
987 1094 'scope' => 'price_range'
988 - ]);
1095 + ]));
989 1096 ?>
990 - <?php
991 - $aria_label = sprintf(
992 - /* translators: 1: Minimum price, 2: Maximum price */
993 - __('Price range: %1$s - %2$s', 'fluent-cart'),
994 - Helper::toDecimal($min_price),
995 - Helper::toDecimal($max_price)
996 - );
997 - ?>
998 - <div class="fct-product-prices fct-price-range" role="term" aria-label="<?php echo esc_attr($aria_label); ?>">
1097 + <div class="fct-product-prices fct-price-range">
999 1098
1000 1099 <?php if ($max_price && $max_price != $min_price && $max_price > $min_price): ?>
1100 + <span class="fct-sr-only"><?php echo esc_html__('Price range:', 'fluent-cart'); ?></span>
1001 1101 <span class="fct-min-price"><?php echo esc_html(Helper::toDecimal($min_price)); ?></span>
1002 1102 <span class="fct-price-separator" aria-hidden="true">-</span>
1103 + <span class="fct-sr-only"><?php echo esc_html__('to', 'fluent-cart'); ?></span>
1003 1104 <?php endif; ?>
1004 1105 <span class="fct-max-price">
1005 1106 <?php echo esc_html(Helper::toDecimal($max_price)); ?>
1006 1107 </span>
1007 1108
1008 - <?php do_action('fluent_cart/product/after_price', [
1109 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
1009 1110 'product' => $this->product,
1010 1111 'current_price' => $min_price,
1011 1112 'scope' => 'price_range'
1012 - ]); ?>
1113 + ])); ?>
1013 1114
1014 1115 </div>
1015 1116 <?php
1016 - do_action('fluent_cart/product/single/after_price_range_block', [
1117 + do_action('fluent_cart/product/single/after_price_range_block', RenderContext::decorate([
1017 1118 'product' => $this->product,
1018 1119 'current_price' => $min_price,
1019 1120 'scope' => 'price_range'
1020 - ]);
1121 + ]));
1021 1122 }
1022 1123
1023 1124 public function renderVariants($atts = [])
1024 1125 {
@@ -1043,19 +1144,19 @@
1043 1144 ?>
1044 1145 <div class="<?php echo esc_attr(implode(' ', $classes)); ?>" role="radiogroup"
1045 1146 aria-label="<?php esc_attr_e('Product Variants', 'fluent-cart'); ?>">
1046 1147 <?php foreach ($variants as $variant) {
1047 - do_action('fluent_cart/product/single/before_variant_item', [
1148 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1048 1149 'product' => $this->product,
1049 1150 'variant' => $variant,
1050 1151 'scope' => 'product_variant_item'
1051 - ]);
1152 + ]));
1052 1153 $this->renderVariationItem($variant, $this->defaultVariationId);
1053 - do_action('fluent_cart/product/single/after_variant_item', [
1154 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1054 1155 'product' => $this->product,
1055 1156 'variant' => $variant,
1056 1157 'scope' => 'product_variant_item'
1057 - ]);
1158 + ]));
1058 1159 } ?>
1059 1160 </div>
1060 1161 <?php
1061 1162 }
@@ -1061,17 +1162,24 @@
1061 1162 }
1062 1163
1063 1164 public function renderItemPrice()
1064 1165 {
1166 + // Same 'price' gate as renderPrices(): one filter hides the price
1167 + // wherever it appears, rather than making callers hunt for the second
1168 + // place the single product page prints one.
1169 + if (!RenderGate::shouldRender('price', RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant))) {
1170 + return;
1171 + }
1172 +
1065 1173 if ($this->product->detail->variation_type === 'simple' && !$this->hasSubscription) {
1066 1174 return; // for simple product we already rendered the price
1067 1175 }
1068 1176
1069 - do_action('fluent_cart/product/single/before_price_block', [
1177 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
1070 1178 'product' => $this->product,
1071 1179 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1072 1180 'scope' => 'product_variant_price'
1073 - ]);
1181 + ]));
1074 1182
1075 1183 foreach ($this->product->variants as $variant) {
1076 1184 if ($this->shouldRenderPriceInPriceSection()) {
1077 1185 $this->renderVariantPricingWrapperStart($variant);
@@ -1105,13 +1213,13 @@
1105 1213
1106 1214
1107 1215
1108 1216
1109 - do_action('fluent_cart/product/single/after_price_block', [
1217 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
1110 1218 'product' => $this->product,
1111 1219 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1112 1220 'scope' => 'product_variant_price'
1113 - ]);
1221 + ]));
1114 1222 }
1115 1223
1116 1224 public function shouldRenderPriceInPriceSection(): bool
1117 1225 {
@@ -1138,14 +1246,15 @@
1138 1246 'product' => $this->product,
1139 1247 'variant' => $variant,
1140 1248 'scope' => 'product_variant_price'
1141 1249 ]));
1142 - do_action('fluent_cart/product/after_price', [
1250 + do_action('fluent_cart/product/after_price', RenderContext::decorate([
1143 1251 'product' => $this->product,
1144 1252 'variant' => $variant,
1145 1253 'current_price' => $variant->item_price,
1146 1254 'scope' => 'product_variant_price'
1147 - ]);
1255 + ]));
1256 + RenderHelper::renderPriceSuffix($this->product, $variant, 'product_variant_price');
1148 1257 }
1149 1258
1150 1259 public function renderComparePriceWrapperStart($atts = [])
1151 1260 {
@@ -1167,8 +1276,9 @@
1167 1276 return;
1168 1277 } ?>
1169 1278
1170 1279 <span class="fct-compare-price">
1280 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1171 1281 <del><?php echo esc_html(Helper::toDecimal($variant->compare_price)); ?></del>
1172 1282 </span>
1173 1283 <?php
1174 1284 }
@@ -1254,8 +1364,14 @@
1254 1364 <?php }
1255 1365
1256 1366 public function renderQuantity()
1257 1367 {
1368 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1369 +
1370 + if (!RenderGate::shouldRender('quantity', $gateContext)) {
1371 + return;
1372 + }
1373 +
1258 1374 $soldIndividually = $this->product->soldIndividually();
1259 1375
1260 1376 if (!$this->hasOnetime || $soldIndividually) {
1261 1377 return;
@@ -1274,12 +1390,12 @@
1274 1390 if ($this->hasSubscription && Arr::get($defaultVariantData, 'payment_type') !== 'onetime') {
1275 1391 $attributes['class'] .= ' is-hidden';
1276 1392 }
1277 1393
1278 - do_action('fluent_cart/product/single/before_quantity_block', [
1394 + do_action('fluent_cart/product/single/before_quantity_block', RenderContext::decorate([
1279 1395 'product' => $this->product,
1280 1396 'scope' => 'product_quantity_block'
1281 - ]);
1397 + ]));
1282 1398 ?>
1283 1399 <div <?php $this->renderAttributes($attributes); ?>>
1284 1400 <label for="fct-product-qty-input" class="quantity-title">
1285 1401 <?php esc_html_e('Quantity', 'fluent-cart'); ?>
@@ -1323,23 +1439,39 @@
1323 1439 </button>
1324 1440 </div>
1325 1441 </div>
1326 1442 <?php
1327 - do_action('fluent_cart/product/single/after_quantity_block', [
1443 + do_action('fluent_cart/product/single/after_quantity_block', RenderContext::decorate([
1328 1444 'product' => $this->product,
1329 1445 'scope' => 'product_quantity_block'
1330 - ]);
1446 + ]));
1331 1447 }
1332 1448
1333 1449 public function renderPurchaseButtons($atts = [])
1334 1450 {
1451 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1452 +
1453 + if (!RenderGate::shouldRender('actions', $gateContext)) {
1454 + return;
1455 + }
1456 +
1457 + do_action('fluent_cart/product/single/before_actions_block', $gateContext);
1458 +
1335 1459 $buyNowButtonAtts = $atts;
1336 1460 $this->renderBuyNowButton($buyNowButtonAtts);
1337 1461 $this->renderAddToCartButton($atts);
1462 +
1463 + do_action('fluent_cart/product/single/after_actions_block', $gateContext);
1338 1464 }
1339 1465
1340 1466 public function renderBuyNowButton($atts = [])
1341 1467 {
1468 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1469 +
1470 + if (!RenderGate::shouldRenderPurchaseButton('buy_now_button', $gateContext)) {
1471 + return;
1472 + }
1473 +
1342 1474 // Stock management check using isStock() method
1343 1475 // if (ModuleSettings::isActive('stock_management')) {
1344 1476 // if ($this->product->detail->variation_type === 'simple' && $this->defaultVariant) {
1345 1477 // if (!$this->defaultVariant->isStock()) {
@@ -1421,8 +1553,16 @@
1421 1553 }
1422 1554
1423 1555 public function renderBuyNowButtonBlock($atts = [])
1424 1556 {
1557 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1558 +
1559 + // Same gate as the in-section button: a page assembled from standalone
1560 + // button blocks must honour catalog mode too, or the gate leaks.
1561 + if (!RenderGate::shouldRenderPurchaseButton('buy_now_button', $gateContext)) {
1562 + return;
1563 + }
1564 +
1425 1565 $text = Arr::get($atts, 'text', __('Buy Now', 'fluent-cart'));
1426 1566 $variantIds = Arr::get($atts, 'variant_ids', []);
1427 1567 $variantId = Arr::get($variantIds, 0);
1428 1568 $customClass = trim(Arr::get($atts, 'class', ''));
@@ -1507,8 +1647,14 @@
1507 1647 }
1508 1648
1509 1649 public function renderAddToCartButton($atts = [])
1510 1650 {
1651 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1652 +
1653 + if (!RenderGate::shouldRenderPurchaseButton('add_to_cart_button', $gateContext)) {
1654 + return;
1655 + }
1656 +
1511 1657 $defaults = [
1512 1658 'buy_now_text' => __('Buy Now', 'fluent-cart'),
1513 1659 'add_to_cart_text' => __('Add To Cart', 'fluent-cart'),
1514 1660 ];
@@ -1603,8 +1749,15 @@
1603 1749 }
1604 1750
1605 1751 public function renderAddToCartButtonBlock($atts = [])
1606 1752 {
1753 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1754 +
1755 + // Same gate as the in-section button — see renderBuyNowButtonBlock().
1756 + if (!RenderGate::shouldRenderPurchaseButton('add_to_cart_button', $gateContext)) {
1757 + return;
1758 + }
1759 +
1607 1760 $text = Arr::get($atts, 'text', __('Add To Cart', 'fluent-cart'));
1608 1761 $customClass = trim(Arr::get($atts, 'class', ''));
1609 1762 $extraClass = trim(Arr::get($atts, 'extra_class', ''));
1610 1763
@@ -1738,14 +1891,8 @@
1738 1891 if ($variant->id == $defaultId) {
1739 1892 $itemClasses[] = 'selected';
1740 1893 }
1741 1894
1742 - $priceSuffix = apply_filters('fluent_cart/product/price_suffix_atts', '', [
1743 - 'product' => $this->product,
1744 - 'variant' => $variant,
1745 - 'scope' => 'variant_item'
1746 - ]);
1747 -
1748 1895 $renderingAttributes = [
1749 1896 'data-fluent-cart-product-variant' => '',
1750 1897 'data-cart-id' => $variant->id,
1751 1898 'data-item-stock' => $variant->isStock() ? 'in-stock' : 'out-of-stock',
@@ -1753,9 +1900,8 @@
1753 1900 'data-payment-type' => $paymentType,
1754 1901 'data-available-stock' => $availableStocks,
1755 1902 'data-item-price' => Helper::toDecimal($variant->item_price),
1756 1903 'data-compare-price' => $comparePrice,
1757 - 'data-price-suffix' => $priceSuffix,
1758 1904 'data-stock-management' => ModuleSettings::isActive('stock_management') ? 'yes' : 'no',
1759 1905 'data-sku' => $variant->sku ?? '',
1760 1906 'data-package-info' => $this->getVariantPackageInfoJson($variant),
1761 1907 ];
@@ -1803,9 +1949,9 @@
1803 1949 }
1804 1950 ?>
1805 1951 <?php if ($this->viewType === 'both' || $this->viewType === 'text'): ?>
1806 1952 <div class="fct-product-variant-text">
1807 - <div class="fct-product-variant-title" aria-label="<?php echo esc_attr(__('Variant title', 'fluent-cart')); ?>"><?php echo esc_html($variant->variation_title); ?></div>
1953 + <div class="fct-product-variant-title"><?php echo esc_html($variant->variation_title); ?></div>
1808 1954 <?php if (!$this->shouldRenderPriceInPriceSection() && $paymentType === 'subscription'): ?>
1809 1955 <?php $this->renderSubscriptionInfo($variant); ?>
1810 1956 <?php endif; ?>
1811 1957 </div>
@@ -1815,14 +1961,15 @@
1815 1961 <?php if (!$this->shouldRenderPriceInPriceSection()): ?>
1816 1962 <div class="fct-product-variant-price">
1817 1963 <?php if ($comparePrice): ?>
1818 1964 <div class="fct-product-variant-compare-price">
1819 - <del aria-label="<?php echo esc_attr(__('Original price', 'fluent-cart')); ?>">
1965 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1966 + <del>
1820 1967 <span><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></span></del>
1821 1968 </div>
1822 1969 <?php endif; ?>
1823 - <div class="fct-product-variant-item-price"
1824 - aria-label="<?php echo esc_attr(__('Current price', 'fluent-cart')); ?>">
1970 + <div class="fct-product-variant-item-price">
1971 + <span class="fct-sr-only"><?php echo $comparePrice ? esc_html__('Sale price:', 'fluent-cart') : esc_html__('Price:', 'fluent-cart'); ?></span>
1825 1972 <span><?php echo esc_html(Helper::toDecimal($itemPrice)); ?></span>
1826 1973 </div>
1827 1974 </div>
1828 1975 <?php endif; ?>
@@ -1946,21 +2093,21 @@
1946 2093 //Convert to collection safely before sorting
1947 2094 $variants = (new Collection($variants))->sortBy('serial_index')->values();
1948 2095
1949 2096 foreach ($variants as $variant) {
1950 - do_action('fluent_cart/product/single/before_variant_item', [
2097 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1951 2098 'product' => $this->product,
1952 2099 'variant' => $variant,
1953 2100 'scope' => 'product_variant_item'
1954 - ]);
2101 + ]));
1955 2102
1956 2103 $this->renderVariationItem($variant, $this->defaultVariationId);
1957 2104
1958 - do_action('fluent_cart/product/single/after_variant_item', [
2105 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1959 2106 'product' => $this->product,
1960 2107 'variant' => $variant,
1961 2108 'scope' => 'product_variant_item'
1962 - ]);
2109 + ]));
1963 2110 }
1964 2111 ?>
1965 2112 </div>
1966 2113