PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.6
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.6
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 1.3.19 All 49 releases
← All changes | app/Services/Renderer/ProductRenderer.php +229 -92 1.5.3 → 1.6.6 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 {
@@ -729,21 +806,40 @@
729 806
730 807 $stockLabel = Arr::get($stockAvailability, 'availability');
731 808 $statusClass = $stockAvailability['class'] ?? '';
732 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 +
733 818 // The variant-level check above can override the aggregate stock_availability
734 819 // used for $stockLabel/$statusClass (e.g. this specific default variant is out
735 820 // of stock even though the product overall has other in-stock variants) — keep
736 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.
737 824 if (!$isStock) {
738 825 $statusClass = 'out-of-stock';
739 - $stockLabel = __('Out of Stock', 'fluent-cart');
826 + $stockLabel = !empty($outOfStockText) ? $outOfStockText : __('Out of Stock', 'fluent-cart');
740 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 +
741 837 echo sprintf(
742 838 '<div class="fct-product-stock %1$s" role="status" aria-live="polite">
743 839 <div %2$s>
744 840 <span class="fct-stock-label">%3$s</span>
745 - <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>
746 842 %4$s
747 843 </span>
748 844 </div>
749 845 </div>',
@@ -749,9 +845,10 @@
749 845 </div>',
750 846 esc_attr($statusClass),
751 847 $wrapper_attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
752 848 esc_html__('Availability:', 'fluent-cart'),
753 - esc_html($stockLabel)
849 + esc_html($stockLabel),
850 + $badgeAttributes // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- values passed through esc_attr() above
754 851 );
755 852 }
756 853
757 854 public function renderSku($wrapper_attributes = '', $showLabel = true, $label = '', $variant = null)
@@ -801,23 +898,24 @@
801 898 );
802 899 }
803 900 }
804 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 + */
805 907 public function renderPackageDescription($wrapper_attributes = '', $showName = true, $showDimensions = true, $showProductWeight = true, $showTotalWeight = true, $variant = null)
806 908 {
807 - if ($variant) {
808 - if (!$wrapper_attributes) {
809 - $wrapper_attributes = 'class="fct-package-description" data-fluent-cart-package-description';
810 - }
811 - (new ProductCardRender($this->product))->renderPackageDescription($wrapper_attributes, $showName, $showDimensions, $showProductWeight, $showTotalWeight, $variant);
812 - return;
813 - }
814 -
815 - foreach ($this->product->variants as $v) {
816 - $isHidden = ($this->defaultVariant && $this->defaultVariant->id != $v->id) ? ' is-hidden' : '';
817 - $variantWrapper = 'class="fct-package-description fluent-cart-product-variation-content' . $isHidden . '" data-fluent-cart-package-description data-variation-id="' . esc_attr($v->id) . '"';
818 - (new ProductCardRender($this->product))->renderPackageDescription($variantWrapper, $showName, $showDimensions, $showProductWeight, $showTotalWeight, $v);
819 - }
909 + (new PackageDescriptionRenderer($this->product))->renderPackageDescription(
910 + $wrapper_attributes,
911 + $showName,
912 + $showDimensions,
913 + $showProductWeight,
914 + $showTotalWeight,
915 + $variant,
916 + $this->defaultVariant
917 + );
820 918 }
821 919
822 920 /**
823 921 * Build a JSON string of package info for a variant (used as data attribute for JS switching).
@@ -886,14 +984,22 @@
886 984 $excerpt = $this->product->post_excerpt;
887 985 if (!$excerpt) {
888 986 return;
889 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);
890 996 ?>
891 997 <div class="fct-product-excerpt" aria-labelledby="fct-product-summary-title">
892 998 <p><?php echo wp_kses_post($excerpt); ?></p>
893 999 </div>
894 1000 <?php
895 -
1001 + do_action('fluent_cart/product/single/after_excerpt_block', $gateContext);
896 1002 }
897 1003
898 1004 public function renderDescription()
899 1005 {
@@ -923,8 +1029,14 @@
923 1029 }
924 1030
925 1031 public function renderPrices()
926 1032 {
1033 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1034 +
1035 + if (!RenderGate::shouldRender('price', $gateContext)) {
1036 + return;
1037 + }
1038 +
927 1039 if ($this->product->detail->variation_type === 'simple') {
928 1040 // we have to render for the simple product
929 1041
930 1042 $first_price = $this->product->variants()->first();
@@ -938,97 +1050,76 @@
938 1050 $comparePrice = $first_price ? (int)$first_price->compare_price : 0;
939 1051 if ($comparePrice <= $itemPrice) {
940 1052 $comparePrice = 0;
941 1053 }
942 - do_action('fluent_cart/product/single/before_price_block', [
1054 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
943 1055 'product' => $this->product,
944 1056 'current_price' => $itemPrice,
945 1057 'scope' => 'price_range'
946 - ]);
1058 + ]));
947 1059 ?>
948 - <?php
1060 + <div class="fct-price-range fct-product-prices">
949 1061
950 - if ($comparePrice) {
951 - $aria_label = sprintf(
952 - /* translators: 1: Original price, 2: Current item price */
953 - __('Original Price: %1$s, Price: %2$s', 'fluent-cart'),
954 - Helper::toDecimal($comparePrice),
955 - Helper::toDecimal($itemPrice)
956 - );
957 - } else {
958 - $aria_label = sprintf(
959 - /* translators: 1: Current item price */
960 - __('Price: %1$s', 'fluent-cart'),
961 - Helper::toDecimal($itemPrice)
962 - );
963 - }
964 -
965 - ?>
966 - <div class="fct-price-range fct-product-prices" role="term"
967 - aria-label="<?php echo esc_attr($aria_label); ?>">
968 -
969 1062 <?php if ($comparePrice): ?>
970 1063 <span class="fct-compare-price">
971 - <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>
972 1066 </span>
973 1067 <?php endif; ?>
974 - <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>
975 1070 <?php echo esc_html(Helper::toDecimal($itemPrice)); ?>
976 - <?php do_action('fluent_cart/product/after_price', [
1071 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
977 1072 'product' => $this->product,
1073 + 'variant' => $first_price,
978 1074 'current_price' => $itemPrice,
979 1075 'scope' => 'price_range'
980 - ]); ?>
1076 + ])); ?>
1077 + <?php RenderHelper::renderPriceSuffix($this->product, $first_price, 'price_range'); ?>
981 1078 </span>
982 1079 </div>
983 1080 <?php
984 - do_action('fluent_cart/product/single/after_price_block', [
1081 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
985 1082 'product' => $this->product,
986 1083 'current_price' => $itemPrice,
987 1084 'scope' => 'price_range'
988 - ]);
1085 + ]));
989 1086 return;
990 1087 }
991 1088 $min_price = $this->product->detail->min_price;
992 1089 $max_price = $this->product->detail->max_price;
993 1090
994 - do_action('fluent_cart/product/single/before_price_range_block', [
1091 + do_action('fluent_cart/product/single/before_price_range_block', RenderContext::decorate([
995 1092 'product' => $this->product,
996 1093 'current_price' => $min_price,
997 1094 'scope' => 'price_range'
998 - ]);
1095 + ]));
999 1096 ?>
1000 - <?php
1001 - $aria_label = sprintf(
1002 - /* translators: 1: Minimum price, 2: Maximum price */
1003 - __('Price range: %1$s - %2$s', 'fluent-cart'),
1004 - Helper::toDecimal($min_price),
1005 - Helper::toDecimal($max_price)
1006 - );
1007 - ?>
1008 - <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">
1009 1098
1010 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>
1011 1101 <span class="fct-min-price"><?php echo esc_html(Helper::toDecimal($min_price)); ?></span>
1012 1102 <span class="fct-price-separator" aria-hidden="true">-</span>
1103 + <span class="fct-sr-only"><?php echo esc_html__('to', 'fluent-cart'); ?></span>
1013 1104 <?php endif; ?>
1014 1105 <span class="fct-max-price">
1015 1106 <?php echo esc_html(Helper::toDecimal($max_price)); ?>
1016 1107 </span>
1017 1108
1018 - <?php do_action('fluent_cart/product/after_price', [
1109 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
1019 1110 'product' => $this->product,
1020 1111 'current_price' => $min_price,
1021 1112 'scope' => 'price_range'
1022 - ]); ?>
1113 + ])); ?>
1023 1114
1024 1115 </div>
1025 1116 <?php
1026 - do_action('fluent_cart/product/single/after_price_range_block', [
1117 + do_action('fluent_cart/product/single/after_price_range_block', RenderContext::decorate([
1027 1118 'product' => $this->product,
1028 1119 'current_price' => $min_price,
1029 1120 'scope' => 'price_range'
1030 - ]);
1121 + ]));
1031 1122 }
1032 1123
1033 1124 public function renderVariants($atts = [])
1034 1125 {
@@ -1053,19 +1144,19 @@
1053 1144 ?>
1054 1145 <div class="<?php echo esc_attr(implode(' ', $classes)); ?>" role="radiogroup"
1055 1146 aria-label="<?php esc_attr_e('Product Variants', 'fluent-cart'); ?>">
1056 1147 <?php foreach ($variants as $variant) {
1057 - do_action('fluent_cart/product/single/before_variant_item', [
1148 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1058 1149 'product' => $this->product,
1059 1150 'variant' => $variant,
1060 1151 'scope' => 'product_variant_item'
1061 - ]);
1152 + ]));
1062 1153 $this->renderVariationItem($variant, $this->defaultVariationId);
1063 - do_action('fluent_cart/product/single/after_variant_item', [
1154 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1064 1155 'product' => $this->product,
1065 1156 'variant' => $variant,
1066 1157 'scope' => 'product_variant_item'
1067 - ]);
1158 + ]));
1068 1159 } ?>
1069 1160 </div>
1070 1161 <?php
1071 1162 }
@@ -1071,17 +1162,24 @@
1071 1162 }
1072 1163
1073 1164 public function renderItemPrice()
1074 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 +
1075 1173 if ($this->product->detail->variation_type === 'simple' && !$this->hasSubscription) {
1076 1174 return; // for simple product we already rendered the price
1077 1175 }
1078 1176
1079 - do_action('fluent_cart/product/single/before_price_block', [
1177 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
1080 1178 'product' => $this->product,
1081 1179 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1082 1180 'scope' => 'product_variant_price'
1083 - ]);
1181 + ]));
1084 1182
1085 1183 foreach ($this->product->variants as $variant) {
1086 1184 if ($this->shouldRenderPriceInPriceSection()) {
1087 1185 $this->renderVariantPricingWrapperStart($variant);
@@ -1115,13 +1213,13 @@
1115 1213
1116 1214
1117 1215
1118 1216
1119 - do_action('fluent_cart/product/single/after_price_block', [
1217 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
1120 1218 'product' => $this->product,
1121 1219 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1122 1220 'scope' => 'product_variant_price'
1123 - ]);
1221 + ]));
1124 1222 }
1125 1223
1126 1224 public function shouldRenderPriceInPriceSection(): bool
1127 1225 {
@@ -1148,14 +1246,15 @@
1148 1246 'product' => $this->product,
1149 1247 'variant' => $variant,
1150 1248 'scope' => 'product_variant_price'
1151 1249 ]));
1152 - do_action('fluent_cart/product/after_price', [
1250 + do_action('fluent_cart/product/after_price', RenderContext::decorate([
1153 1251 'product' => $this->product,
1154 1252 'variant' => $variant,
1155 1253 'current_price' => $variant->item_price,
1156 1254 'scope' => 'product_variant_price'
1157 - ]);
1255 + ]));
1256 + RenderHelper::renderPriceSuffix($this->product, $variant, 'product_variant_price');
1158 1257 }
1159 1258
1160 1259 public function renderComparePriceWrapperStart($atts = [])
1161 1260 {
@@ -1177,8 +1276,9 @@
1177 1276 return;
1178 1277 } ?>
1179 1278
1180 1279 <span class="fct-compare-price">
1280 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1181 1281 <del><?php echo esc_html(Helper::toDecimal($variant->compare_price)); ?></del>
1182 1282 </span>
1183 1283 <?php
1184 1284 }
@@ -1264,8 +1364,14 @@
1264 1364 <?php }
1265 1365
1266 1366 public function renderQuantity()
1267 1367 {
1368 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1369 +
1370 + if (!RenderGate::shouldRender('quantity', $gateContext)) {
1371 + return;
1372 + }
1373 +
1268 1374 $soldIndividually = $this->product->soldIndividually();
1269 1375
1270 1376 if (!$this->hasOnetime || $soldIndividually) {
1271 1377 return;
@@ -1284,12 +1390,12 @@
1284 1390 if ($this->hasSubscription && Arr::get($defaultVariantData, 'payment_type') !== 'onetime') {
1285 1391 $attributes['class'] .= ' is-hidden';
1286 1392 }
1287 1393
1288 - do_action('fluent_cart/product/single/before_quantity_block', [
1394 + do_action('fluent_cart/product/single/before_quantity_block', RenderContext::decorate([
1289 1395 'product' => $this->product,
1290 1396 'scope' => 'product_quantity_block'
1291 - ]);
1397 + ]));
1292 1398 ?>
1293 1399 <div <?php $this->renderAttributes($attributes); ?>>
1294 1400 <label for="fct-product-qty-input" class="quantity-title">
1295 1401 <?php esc_html_e('Quantity', 'fluent-cart'); ?>
@@ -1333,23 +1439,39 @@
1333 1439 </button>
1334 1440 </div>
1335 1441 </div>
1336 1442 <?php
1337 - do_action('fluent_cart/product/single/after_quantity_block', [
1443 + do_action('fluent_cart/product/single/after_quantity_block', RenderContext::decorate([
1338 1444 'product' => $this->product,
1339 1445 'scope' => 'product_quantity_block'
1340 - ]);
1446 + ]));
1341 1447 }
1342 1448
1343 1449 public function renderPurchaseButtons($atts = [])
1344 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 +
1345 1459 $buyNowButtonAtts = $atts;
1346 1460 $this->renderBuyNowButton($buyNowButtonAtts);
1347 1461 $this->renderAddToCartButton($atts);
1462 +
1463 + do_action('fluent_cart/product/single/after_actions_block', $gateContext);
1348 1464 }
1349 1465
1350 1466 public function renderBuyNowButton($atts = [])
1351 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 +
1352 1474 // Stock management check using isStock() method
1353 1475 // if (ModuleSettings::isActive('stock_management')) {
1354 1476 // if ($this->product->detail->variation_type === 'simple' && $this->defaultVariant) {
1355 1477 // if (!$this->defaultVariant->isStock()) {
@@ -1431,8 +1553,16 @@
1431 1553 }
1432 1554
1433 1555 public function renderBuyNowButtonBlock($atts = [])
1434 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 +
1435 1565 $text = Arr::get($atts, 'text', __('Buy Now', 'fluent-cart'));
1436 1566 $variantIds = Arr::get($atts, 'variant_ids', []);
1437 1567 $variantId = Arr::get($variantIds, 0);
1438 1568 $customClass = trim(Arr::get($atts, 'class', ''));
@@ -1517,8 +1647,14 @@
1517 1647 }
1518 1648
1519 1649 public function renderAddToCartButton($atts = [])
1520 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 +
1521 1657 $defaults = [
1522 1658 'buy_now_text' => __('Buy Now', 'fluent-cart'),
1523 1659 'add_to_cart_text' => __('Add To Cart', 'fluent-cart'),
1524 1660 ];
@@ -1613,8 +1749,15 @@
1613 1749 }
1614 1750
1615 1751 public function renderAddToCartButtonBlock($atts = [])
1616 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 +
1617 1760 $text = Arr::get($atts, 'text', __('Add To Cart', 'fluent-cart'));
1618 1761 $customClass = trim(Arr::get($atts, 'class', ''));
1619 1762 $extraClass = trim(Arr::get($atts, 'extra_class', ''));
1620 1763
@@ -1748,14 +1891,8 @@
1748 1891 if ($variant->id == $defaultId) {
1749 1892 $itemClasses[] = 'selected';
1750 1893 }
1751 1894
1752 - $priceSuffix = apply_filters('fluent_cart/product/price_suffix_atts', '', [
1753 - 'product' => $this->product,
1754 - 'variant' => $variant,
1755 - 'scope' => 'variant_item'
1756 - ]);
1757 -
1758 1895 $renderingAttributes = [
1759 1896 'data-fluent-cart-product-variant' => '',
1760 1897 'data-cart-id' => $variant->id,
1761 1898 'data-item-stock' => $variant->isStock() ? 'in-stock' : 'out-of-stock',
@@ -1763,9 +1900,8 @@
1763 1900 'data-payment-type' => $paymentType,
1764 1901 'data-available-stock' => $availableStocks,
1765 1902 'data-item-price' => Helper::toDecimal($variant->item_price),
1766 1903 'data-compare-price' => $comparePrice,
1767 - 'data-price-suffix' => $priceSuffix,
1768 1904 'data-stock-management' => ModuleSettings::isActive('stock_management') ? 'yes' : 'no',
1769 1905 'data-sku' => $variant->sku ?? '',
1770 1906 'data-package-info' => $this->getVariantPackageInfoJson($variant),
1771 1907 ];
@@ -1813,9 +1949,9 @@
1813 1949 }
1814 1950 ?>
1815 1951 <?php if ($this->viewType === 'both' || $this->viewType === 'text'): ?>
1816 1952 <div class="fct-product-variant-text">
1817 - <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>
1818 1954 <?php if (!$this->shouldRenderPriceInPriceSection() && $paymentType === 'subscription'): ?>
1819 1955 <?php $this->renderSubscriptionInfo($variant); ?>
1820 1956 <?php endif; ?>
1821 1957 </div>
@@ -1825,14 +1961,15 @@
1825 1961 <?php if (!$this->shouldRenderPriceInPriceSection()): ?>
1826 1962 <div class="fct-product-variant-price">
1827 1963 <?php if ($comparePrice): ?>
1828 1964 <div class="fct-product-variant-compare-price">
1829 - <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>
1830 1967 <span><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></span></del>
1831 1968 </div>
1832 1969 <?php endif; ?>
1833 - <div class="fct-product-variant-item-price"
1834 - 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>
1835 1972 <span><?php echo esc_html(Helper::toDecimal($itemPrice)); ?></span>
1836 1973 </div>
1837 1974 </div>
1838 1975 <?php endif; ?>
@@ -1956,21 +2093,21 @@
1956 2093 //Convert to collection safely before sorting
1957 2094 $variants = (new Collection($variants))->sortBy('serial_index')->values();
1958 2095
1959 2096 foreach ($variants as $variant) {
1960 - do_action('fluent_cart/product/single/before_variant_item', [
2097 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1961 2098 'product' => $this->product,
1962 2099 'variant' => $variant,
1963 2100 'scope' => 'product_variant_item'
1964 - ]);
2101 + ]));
1965 2102
1966 2103 $this->renderVariationItem($variant, $this->defaultVariationId);
1967 2104
1968 - do_action('fluent_cart/product/single/after_variant_item', [
2105 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1969 2106 'product' => $this->product,
1970 2107 'variant' => $variant,
1971 2108 'scope' => 'product_variant_item'
1972 - ]);
2109 + ]));
1973 2110 }
1974 2111 ?>
1975 2112 </div>
1976 2113