PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
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 +208 -86 1.5.4 → 1.7.0 View file →
@@ -3,9 +3,8 @@
3 3 namespace FluentCart\App\Services\Renderer;
4 4
5 5 use FluentCart\App\App;
6 6 use FluentCart\App\Services\FrontendView;
7 -use FluentCart\App\Vite;
8 7 use FluentCart\Api\StoreSettings;
9 8 use FluentCart\Api\ModuleSettings;
10 9 use FluentCart\App\Helpers\Helper;
11 10 use FluentCart\App\Models\Product;
@@ -13,8 +12,9 @@
13 12 use FluentCart\App\Http\Routes\WebRoutes;
14 13 use FluentCart\App\Models\ProductVariation;
15 14 use FluentCart\Framework\Support\Collection;
16 15 use FluentCart\App\Modules\Templating\AssetLoader;
16 +use FluentCart\App\Modules\FluentPlayer\ProductVideoRenderer;
17 17
18 18 class ProductRenderer
19 19 {
20 20 protected $product;
@@ -38,8 +38,10 @@
38 38 protected $defaultGalleryImageId = 0;
39 39
40 40 protected $galleryActiveSet = false;
41 41
42 + protected $productVideoRenderer = null;
43 +
42 44 protected $paymentTypes = [];
43 45
44 46 protected $variantsByPaymentTypes = [];
45 47
@@ -294,8 +296,19 @@
294 296 }
295 297
296 298 public function renderBuySection($atts = [])
297 299 {
300 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
301 +
302 + // The buy section is the root the variation-selector JS binds to
303 + // (data-fluent-cart-product-pricing-section). Gating it removes the
304 + // variation picker along with the buttons, which is what a full catalog
305 + // mode wants; to keep shoppers able to browse options while hiding only
306 + // the purchase affordances, gate 'actions' instead.
307 + if (!RenderGate::shouldRender('buy_section', $gateContext)) {
308 + return;
309 + }
310 +
298 311 // Render no buy section when there is nothing purchasable — avoids a
299 312 // broken quantity + "Not Available" block. Two cases:
300 313 // - no variants at all (any product type), or
301 314 // - an advanced-variation product with no attribute_config yet (e.g.
@@ -359,15 +372,15 @@
359 372 public function renderGalleryThumb()
360 373 {
361 374 $thumbnails = [];
362 375
363 - $featuredMedia = $this->product->thumbnail ?? Vite::getAssetUrl('images/placeholder.svg');
376 + $featuredMedia = $this->product->thumbnail ?? Helper::getProductPlaceholderUrl();
364 377
365 378 // thumbnail can be an empty string (not null), so ?? above doesn't catch
366 379 // it — fall back to the placeholder so $featuredMedia is always a usable
367 380 // image URL for both the main <img> and data-default-image-url.
368 381 if (!$featuredMedia || !\is_string($featuredMedia)) {
369 - $featuredMedia = Vite::getAssetUrl('images/placeholder.svg');
382 + $featuredMedia = Helper::getProductPlaceholderUrl();
370 383 }
371 384
372 385 $galleryImage = get_post_meta($this->product->ID, 'fluent-products-gallery-image', true);
373 386
@@ -421,11 +434,16 @@
421 434 if (empty($this->defaultImageUrl)) {
422 435 $this->defaultImageUrl = $featuredMedia;
423 436 }
424 437
438 + $videoRenderer = $this->getProductVideoRenderer();
439 + $videoRenderer->showFirstByDefault(!$this->hasGalleryImages());
440 + $defaultVideoId = $videoRenderer->getDefaultMediaId();
441 +
425 442 ?>
426 - <div class="fct-product-gallery-thumb" role="region"
427 - aria-label="<?php echo esc_attr($this->product->post_title . ' gallery'); ?>">
443 + <div class="fct-product-gallery-thumb<?php echo $defaultVideoId ? ' is-video-active' : ''; ?>" role="region"
444 + aria-label="<?php echo esc_attr($this->product->post_title . ' gallery'); ?>"
445 + <?php echo $defaultVideoId ? 'data-fct-video-default="' . esc_attr((string) $defaultVideoId) . '"' : ''; ?>>
428 446 <img
429 447 src="<?php echo esc_url($this->defaultImageUrl ?? '') ?>"
430 448 alt="<?php echo esc_attr($this->defaultImageAlt) ?>"
431 449 data-fluent-cart-single-product-page-product-thumbnail
@@ -430,21 +448,54 @@
430 448 alt="<?php echo esc_attr($this->defaultImageAlt) ?>"
431 449 data-fluent-cart-single-product-page-product-thumbnail
432 450 data-default-image-url="<?php echo esc_url($featuredMedia) ?>"
433 451 />
452 + <?php $this->getProductVideoRenderer()->renderInlinePlayers(); ?>
434 453 </div>
435 454 <?php
436 455 }
437 456
457 + /**
458 + * Whether any gallery / variant image exists to show in the main area.
459 + * Only meaningful after renderGalleryThumb() has built $this->images.
460 + */
461 + protected function hasGalleryImages(): bool
462 + {
463 + foreach ($this->images as $image) {
464 + foreach ((array) Arr::get($image, 'media', []) as $item) {
465 + if (!empty(Arr::get($item, 'url'))) {
466 + return true;
467 + }
468 + }
469 + }
470 +
471 + return false;
472 + }
473 +
474 + public function getProductVideoRenderer(): ProductVideoRenderer
475 + {
476 + if ($this->productVideoRenderer === null) {
477 + $this->productVideoRenderer = new ProductVideoRenderer($this->product);
478 + }
479 +
480 + return $this->productVideoRenderer;
481 + }
482 +
438 483 public function renderGalleryThumbControls($maxThumbnails = null)
439 484 {
440 485 $totalThumbImages = Arr::pluck($this->images, 'media.*.url');
441 486
442 - if(count($totalThumbImages) == 1 && is_countable($totalThumbImages[0]) && count($totalThumbImages[0]) == 1){
487 + // A lone image needs no strip, but a video thumb still has to be reachable.
488 + if(count($totalThumbImages) == 1 && is_countable($totalThumbImages[0]) && count($totalThumbImages[0]) == 1 && !$this->getProductVideoRenderer()->isAvailable()){
443 489
444 490 return '';
445 491 }
446 492
493 + // A lone video without images is already showing in the main area.
494 + if (!$this->hasGalleryImages() && count($this->getProductVideoRenderer()->getVideos()) === 1) {
495 + return '';
496 + }
497 +
447 498 // Build variantTermMap and variantFirstMediaMap.
448 499 // For advanced variations, Pro builds both maps via the gallery_variation_data
449 500 // filter — it has access to AttributeGroup types (color/image) that free cannot
450 501 // query directly. For all other product types, free builds variantFirstMediaMap
@@ -537,9 +588,16 @@
537 588 $isAdvVariation = !empty($this->variantTermMap);
538 589 $countedMediaKeys = [];
539 590 $count = 0;
540 591 $totalImages = 0;
592 + $videoRenderer = $this->getProductVideoRenderer();
541 593
594 + // The gallery opens on a video the admin put in front of every image,
595 + // so no image thumb takes the selected slot in that case.
596 + if ($videoRenderer->getDefaultMediaId()) {
597 + $this->galleryActiveSet = true;
598 + }
599 +
542 600 // Count unique images to render. For advanced variations, deduplicate by WP media ID
543 601 // when available, or by URL for externally imported images (media ID = 0).
544 602 foreach ($this->images as $imageId => $image) {
545 603 if (empty($image['media']) || !is_array($image['media'])) {
@@ -581,16 +639,21 @@
581 639 }
582 640 $renderedMediaKeys[$mediaDedupeKey] = true;
583 641 }
584 642 if ($maxThumbnails !== null && $count >= (int) $maxThumbnails) {
643 + $videoRenderer->renderThumbControls(!$this->galleryActiveSet);
585 644 $this->renderGallerySeeMoreButton($totalImages - (int) $maxThumbnails);
586 645 return;
587 646 }
647 + // Videos the admin dragged in front of this image come first.
648 + $videoRenderer->renderThumbControlsBefore($count, !$this->galleryActiveSet);
588 649 $termId = (int) ($this->variantTermMap[(int) $imageId] ?? 0);
589 650 $this->renderGalleryThumbControlButton($item, $imageId, $termId, $mediaId);
590 651 $count++;
591 652 }
592 653 }
654 +
655 + $videoRenderer->renderThumbControls(!$this->galleryActiveSet);
593 656 }
594 657
595 658 public function renderGallerySeeMoreButton($remainingCount)
596 659 {
@@ -663,9 +726,14 @@
663 726 }
664 727
665 728 public function renderGallery($args = [])
666 729 {
730 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
667 731
732 + if (!RenderGate::shouldRender('image', $gateContext)) {
733 + return;
734 + }
735 +
668 736 $defaults = [
669 737 'thumbnail_mode' => 'all', // horizontal, vertical
670 738 'thumb_position' => 'bottom', // bottom, left, right, top
671 739 'scrollable_thumbs' => 'no', // yes / no
@@ -699,13 +767,21 @@
699 767 }
700 768
701 769 public function renderTitle()
702 770 {
771 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
772 +
773 + if (!RenderGate::shouldRender('title', $gateContext)) {
774 + return;
775 + }
776 +
777 + do_action('fluent_cart/product/single/before_title_block', $gateContext);
703 778 ?>
704 779 <div class="fct-product-title">
705 780 <h1 id="fct-product-summary-title"><?php echo esc_html($this->product->post_title); ?></h1>
706 781 </div>
707 782 <?php
783 + do_action('fluent_cart/product/single/after_title_block', $gateContext);
708 784 }
709 785
710 786 public function renderStockAvailability($wrapper_attributes = '')
711 787 {
@@ -821,23 +897,24 @@
821 897 );
822 898 }
823 899 }
824 900
901 + /**
902 + * @deprecated Use PackageDescriptionRenderer::renderPackageDescription() directly.
903 + * Kept as a compatibility shim for external callers (themes, extensions) —
904 + * package-description rendering now lives in PackageDescriptionRenderer.
905 + */
825 906 public function renderPackageDescription($wrapper_attributes = '', $showName = true, $showDimensions = true, $showProductWeight = true, $showTotalWeight = true, $variant = null)
826 907 {
827 - if ($variant) {
828 - if (!$wrapper_attributes) {
829 - $wrapper_attributes = 'class="fct-package-description" data-fluent-cart-package-description';
830 - }
831 - (new ProductCardRender($this->product))->renderPackageDescription($wrapper_attributes, $showName, $showDimensions, $showProductWeight, $showTotalWeight, $variant);
832 - return;
833 - }
834 -
835 - foreach ($this->product->variants as $v) {
836 - $isHidden = ($this->defaultVariant && $this->defaultVariant->id != $v->id) ? ' is-hidden' : '';
837 - $variantWrapper = 'class="fct-package-description fluent-cart-product-variation-content' . $isHidden . '" data-fluent-cart-package-description data-variation-id="' . esc_attr($v->id) . '"';
838 - (new ProductCardRender($this->product))->renderPackageDescription($variantWrapper, $showName, $showDimensions, $showProductWeight, $showTotalWeight, $v);
839 - }
908 + (new PackageDescriptionRenderer($this->product))->renderPackageDescription(
909 + $wrapper_attributes,
910 + $showName,
911 + $showDimensions,
912 + $showProductWeight,
913 + $showTotalWeight,
914 + $variant,
915 + $this->defaultVariant
916 + );
840 917 }
841 918
842 919 /**
843 920 * Build a JSON string of package info for a variant (used as data attribute for JS switching).
@@ -906,14 +983,22 @@
906 983 $excerpt = $this->product->post_excerpt;
907 984 if (!$excerpt) {
908 985 return;
909 986 }
987 +
988 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
989 +
990 + if (!RenderGate::shouldRender('excerpt', $gateContext)) {
991 + return;
992 + }
993 +
994 + do_action('fluent_cart/product/single/before_excerpt_block', $gateContext);
910 995 ?>
911 996 <div class="fct-product-excerpt" aria-labelledby="fct-product-summary-title">
912 997 <p><?php echo wp_kses_post($excerpt); ?></p>
913 998 </div>
914 999 <?php
915 -
1000 + do_action('fluent_cart/product/single/after_excerpt_block', $gateContext);
916 1001 }
917 1002
918 1003 public function renderDescription()
919 1004 {
@@ -943,8 +1028,14 @@
943 1028 }
944 1029
945 1030 public function renderPrices()
946 1031 {
1032 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1033 +
1034 + if (!RenderGate::shouldRender('price', $gateContext)) {
1035 + return;
1036 + }
1037 +
947 1038 if ($this->product->detail->variation_type === 'simple') {
948 1039 // we have to render for the simple product
949 1040
950 1041 $first_price = $this->product->variants()->first();
@@ -958,97 +1049,76 @@
958 1049 $comparePrice = $first_price ? (int)$first_price->compare_price : 0;
959 1050 if ($comparePrice <= $itemPrice) {
960 1051 $comparePrice = 0;
961 1052 }
962 - do_action('fluent_cart/product/single/before_price_block', [
1053 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
963 1054 'product' => $this->product,
964 1055 'current_price' => $itemPrice,
965 1056 'scope' => 'price_range'
966 - ]);
1057 + ]));
967 1058 ?>
968 - <?php
1059 + <div class="fct-price-range fct-product-prices">
969 1060
970 - if ($comparePrice) {
971 - $aria_label = sprintf(
972 - /* translators: 1: Original price, 2: Current item price */
973 - __('Original Price: %1$s, Price: %2$s', 'fluent-cart'),
974 - Helper::toDecimal($comparePrice),
975 - Helper::toDecimal($itemPrice)
976 - );
977 - } else {
978 - $aria_label = sprintf(
979 - /* translators: 1: Current item price */
980 - __('Price: %1$s', 'fluent-cart'),
981 - Helper::toDecimal($itemPrice)
982 - );
983 - }
984 -
985 - ?>
986 - <div class="fct-price-range fct-product-prices" role="term"
987 - aria-label="<?php echo esc_attr($aria_label); ?>">
988 -
989 1061 <?php if ($comparePrice): ?>
990 1062 <span class="fct-compare-price">
991 - <del aria-label="<?php echo esc_attr(__('Original price', 'fluent-cart')); ?>"><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></del>
1063 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1064 + <del><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></del>
992 1065 </span>
993 1066 <?php endif; ?>
994 - <span class="fct-item-price" aria-label="<?php echo esc_attr(__('Current price', 'fluent-cart')); ?>">
1067 + <span class="fct-item-price">
1068 + <span class="fct-sr-only"><?php echo $comparePrice ? esc_html__('Sale price:', 'fluent-cart') : esc_html__('Price:', 'fluent-cart'); ?></span>
995 1069 <?php echo esc_html(Helper::toDecimal($itemPrice)); ?>
996 - <?php do_action('fluent_cart/product/after_price', [
1070 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
997 1071 'product' => $this->product,
1072 + 'variant' => $first_price,
998 1073 'current_price' => $itemPrice,
999 1074 'scope' => 'price_range'
1000 - ]); ?>
1075 + ])); ?>
1076 + <?php RenderHelper::renderPriceSuffix($this->product, $first_price, 'price_range'); ?>
1001 1077 </span>
1002 1078 </div>
1003 1079 <?php
1004 - do_action('fluent_cart/product/single/after_price_block', [
1080 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
1005 1081 'product' => $this->product,
1006 1082 'current_price' => $itemPrice,
1007 1083 'scope' => 'price_range'
1008 - ]);
1084 + ]));
1009 1085 return;
1010 1086 }
1011 1087 $min_price = $this->product->detail->min_price;
1012 1088 $max_price = $this->product->detail->max_price;
1013 1089
1014 - do_action('fluent_cart/product/single/before_price_range_block', [
1090 + do_action('fluent_cart/product/single/before_price_range_block', RenderContext::decorate([
1015 1091 'product' => $this->product,
1016 1092 'current_price' => $min_price,
1017 1093 'scope' => 'price_range'
1018 - ]);
1094 + ]));
1019 1095 ?>
1020 - <?php
1021 - $aria_label = sprintf(
1022 - /* translators: 1: Minimum price, 2: Maximum price */
1023 - __('Price range: %1$s - %2$s', 'fluent-cart'),
1024 - Helper::toDecimal($min_price),
1025 - Helper::toDecimal($max_price)
1026 - );
1027 - ?>
1028 - <div class="fct-product-prices fct-price-range" role="term" aria-label="<?php echo esc_attr($aria_label); ?>">
1096 + <div class="fct-product-prices fct-price-range">
1029 1097
1030 1098 <?php if ($max_price && $max_price != $min_price && $max_price > $min_price): ?>
1099 + <span class="fct-sr-only"><?php echo esc_html__('Price range:', 'fluent-cart'); ?></span>
1031 1100 <span class="fct-min-price"><?php echo esc_html(Helper::toDecimal($min_price)); ?></span>
1032 1101 <span class="fct-price-separator" aria-hidden="true">-</span>
1102 + <span class="fct-sr-only"><?php echo esc_html__('to', 'fluent-cart'); ?></span>
1033 1103 <?php endif; ?>
1034 1104 <span class="fct-max-price">
1035 1105 <?php echo esc_html(Helper::toDecimal($max_price)); ?>
1036 1106 </span>
1037 1107
1038 - <?php do_action('fluent_cart/product/after_price', [
1108 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
1039 1109 'product' => $this->product,
1040 1110 'current_price' => $min_price,
1041 1111 'scope' => 'price_range'
1042 - ]); ?>
1112 + ])); ?>
1043 1113
1044 1114 </div>
1045 1115 <?php
1046 - do_action('fluent_cart/product/single/after_price_range_block', [
1116 + do_action('fluent_cart/product/single/after_price_range_block', RenderContext::decorate([
1047 1117 'product' => $this->product,
1048 1118 'current_price' => $min_price,
1049 1119 'scope' => 'price_range'
1050 - ]);
1120 + ]));
1051 1121 }
1052 1122
1053 1123 public function renderVariants($atts = [])
1054 1124 {
@@ -1073,19 +1143,19 @@
1073 1143 ?>
1074 1144 <div class="<?php echo esc_attr(implode(' ', $classes)); ?>" role="radiogroup"
1075 1145 aria-label="<?php esc_attr_e('Product Variants', 'fluent-cart'); ?>">
1076 1146 <?php foreach ($variants as $variant) {
1077 - do_action('fluent_cart/product/single/before_variant_item', [
1147 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1078 1148 'product' => $this->product,
1079 1149 'variant' => $variant,
1080 1150 'scope' => 'product_variant_item'
1081 - ]);
1151 + ]));
1082 1152 $this->renderVariationItem($variant, $this->defaultVariationId);
1083 - do_action('fluent_cart/product/single/after_variant_item', [
1153 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1084 1154 'product' => $this->product,
1085 1155 'variant' => $variant,
1086 1156 'scope' => 'product_variant_item'
1087 - ]);
1157 + ]));
1088 1158 } ?>
1089 1159 </div>
1090 1160 <?php
1091 1161 }
@@ -1091,17 +1161,24 @@
1091 1161 }
1092 1162
1093 1163 public function renderItemPrice()
1094 1164 {
1165 + // Same 'price' gate as renderPrices(): one filter hides the price
1166 + // wherever it appears, rather than making callers hunt for the second
1167 + // place the single product page prints one.
1168 + if (!RenderGate::shouldRender('price', RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant))) {
1169 + return;
1170 + }
1171 +
1095 1172 if ($this->product->detail->variation_type === 'simple' && !$this->hasSubscription) {
1096 1173 return; // for simple product we already rendered the price
1097 1174 }
1098 1175
1099 - do_action('fluent_cart/product/single/before_price_block', [
1176 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
1100 1177 'product' => $this->product,
1101 1178 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1102 1179 'scope' => 'product_variant_price'
1103 - ]);
1180 + ]));
1104 1181
1105 1182 foreach ($this->product->variants as $variant) {
1106 1183 if ($this->shouldRenderPriceInPriceSection()) {
1107 1184 $this->renderVariantPricingWrapperStart($variant);
@@ -1135,13 +1212,13 @@
1135 1212
1136 1213
1137 1214
1138 1215
1139 - do_action('fluent_cart/product/single/after_price_block', [
1216 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
1140 1217 'product' => $this->product,
1141 1218 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1142 1219 'scope' => 'product_variant_price'
1143 - ]);
1220 + ]));
1144 1221 }
1145 1222
1146 1223 public function shouldRenderPriceInPriceSection(): bool
1147 1224 {
@@ -1168,14 +1245,14 @@
1168 1245 'product' => $this->product,
1169 1246 'variant' => $variant,
1170 1247 'scope' => 'product_variant_price'
1171 1248 ]));
1172 - do_action('fluent_cart/product/after_price', [
1249 + do_action('fluent_cart/product/after_price', RenderContext::decorate([
1173 1250 'product' => $this->product,
1174 1251 'variant' => $variant,
1175 1252 'current_price' => $variant->item_price,
1176 1253 'scope' => 'product_variant_price'
1177 - ]);
1254 + ]));
1178 1255 RenderHelper::renderPriceSuffix($this->product, $variant, 'product_variant_price');
1179 1256 }
1180 1257
1181 1258 public function renderComparePriceWrapperStart($atts = [])
@@ -1198,8 +1275,9 @@
1198 1275 return;
1199 1276 } ?>
1200 1277
1201 1278 <span class="fct-compare-price">
1279 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1202 1280 <del><?php echo esc_html(Helper::toDecimal($variant->compare_price)); ?></del>
1203 1281 </span>
1204 1282 <?php
1205 1283 }
@@ -1285,8 +1363,14 @@
1285 1363 <?php }
1286 1364
1287 1365 public function renderQuantity()
1288 1366 {
1367 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1368 +
1369 + if (!RenderGate::shouldRender('quantity', $gateContext)) {
1370 + return;
1371 + }
1372 +
1289 1373 $soldIndividually = $this->product->soldIndividually();
1290 1374
1291 1375 if (!$this->hasOnetime || $soldIndividually) {
1292 1376 return;
@@ -1305,12 +1389,12 @@
1305 1389 if ($this->hasSubscription && Arr::get($defaultVariantData, 'payment_type') !== 'onetime') {
1306 1390 $attributes['class'] .= ' is-hidden';
1307 1391 }
1308 1392
1309 - do_action('fluent_cart/product/single/before_quantity_block', [
1393 + do_action('fluent_cart/product/single/before_quantity_block', RenderContext::decorate([
1310 1394 'product' => $this->product,
1311 1395 'scope' => 'product_quantity_block'
1312 - ]);
1396 + ]));
1313 1397 ?>
1314 1398 <div <?php $this->renderAttributes($attributes); ?>>
1315 1399 <label for="fct-product-qty-input" class="quantity-title">
1316 1400 <?php esc_html_e('Quantity', 'fluent-cart'); ?>
@@ -1354,23 +1438,39 @@
1354 1438 </button>
1355 1439 </div>
1356 1440 </div>
1357 1441 <?php
1358 - do_action('fluent_cart/product/single/after_quantity_block', [
1442 + do_action('fluent_cart/product/single/after_quantity_block', RenderContext::decorate([
1359 1443 'product' => $this->product,
1360 1444 'scope' => 'product_quantity_block'
1361 - ]);
1445 + ]));
1362 1446 }
1363 1447
1364 1448 public function renderPurchaseButtons($atts = [])
1365 1449 {
1450 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1451 +
1452 + if (!RenderGate::shouldRender('actions', $gateContext)) {
1453 + return;
1454 + }
1455 +
1456 + do_action('fluent_cart/product/single/before_actions_block', $gateContext);
1457 +
1366 1458 $buyNowButtonAtts = $atts;
1367 1459 $this->renderBuyNowButton($buyNowButtonAtts);
1368 1460 $this->renderAddToCartButton($atts);
1461 +
1462 + do_action('fluent_cart/product/single/after_actions_block', $gateContext);
1369 1463 }
1370 1464
1371 1465 public function renderBuyNowButton($atts = [])
1372 1466 {
1467 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1468 +
1469 + if (!RenderGate::shouldRenderPurchaseButton('buy_now_button', $gateContext)) {
1470 + return;
1471 + }
1472 +
1373 1473 // Stock management check using isStock() method
1374 1474 // if (ModuleSettings::isActive('stock_management')) {
1375 1475 // if ($this->product->detail->variation_type === 'simple' && $this->defaultVariant) {
1376 1476 // if (!$this->defaultVariant->isStock()) {
@@ -1452,8 +1552,16 @@
1452 1552 }
1453 1553
1454 1554 public function renderBuyNowButtonBlock($atts = [])
1455 1555 {
1556 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1557 +
1558 + // Same gate as the in-section button: a page assembled from standalone
1559 + // button blocks must honour catalog mode too, or the gate leaks.
1560 + if (!RenderGate::shouldRenderPurchaseButton('buy_now_button', $gateContext)) {
1561 + return;
1562 + }
1563 +
1456 1564 $text = Arr::get($atts, 'text', __('Buy Now', 'fluent-cart'));
1457 1565 $variantIds = Arr::get($atts, 'variant_ids', []);
1458 1566 $variantId = Arr::get($variantIds, 0);
1459 1567 $customClass = trim(Arr::get($atts, 'class', ''));
@@ -1538,8 +1646,14 @@
1538 1646 }
1539 1647
1540 1648 public function renderAddToCartButton($atts = [])
1541 1649 {
1650 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1651 +
1652 + if (!RenderGate::shouldRenderPurchaseButton('add_to_cart_button', $gateContext)) {
1653 + return;
1654 + }
1655 +
1542 1656 $defaults = [
1543 1657 'buy_now_text' => __('Buy Now', 'fluent-cart'),
1544 1658 'add_to_cart_text' => __('Add To Cart', 'fluent-cart'),
1545 1659 ];
@@ -1634,8 +1748,15 @@
1634 1748 }
1635 1749
1636 1750 public function renderAddToCartButtonBlock($atts = [])
1637 1751 {
1752 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1753 +
1754 + // Same gate as the in-section button — see renderBuyNowButtonBlock().
1755 + if (!RenderGate::shouldRenderPurchaseButton('add_to_cart_button', $gateContext)) {
1756 + return;
1757 + }
1758 +
1638 1759 $text = Arr::get($atts, 'text', __('Add To Cart', 'fluent-cart'));
1639 1760 $customClass = trim(Arr::get($atts, 'class', ''));
1640 1761 $extraClass = trim(Arr::get($atts, 'extra_class', ''));
1641 1762
@@ -1827,9 +1948,9 @@
1827 1948 }
1828 1949 ?>
1829 1950 <?php if ($this->viewType === 'both' || $this->viewType === 'text'): ?>
1830 1951 <div class="fct-product-variant-text">
1831 - <div class="fct-product-variant-title" aria-label="<?php echo esc_attr(__('Variant title', 'fluent-cart')); ?>"><?php echo esc_html($variant->variation_title); ?></div>
1952 + <div class="fct-product-variant-title"><?php echo esc_html($variant->variation_title); ?></div>
1832 1953 <?php if (!$this->shouldRenderPriceInPriceSection() && $paymentType === 'subscription'): ?>
1833 1954 <?php $this->renderSubscriptionInfo($variant); ?>
1834 1955 <?php endif; ?>
1835 1956 </div>
@@ -1839,14 +1960,15 @@
1839 1960 <?php if (!$this->shouldRenderPriceInPriceSection()): ?>
1840 1961 <div class="fct-product-variant-price">
1841 1962 <?php if ($comparePrice): ?>
1842 1963 <div class="fct-product-variant-compare-price">
1843 - <del aria-label="<?php echo esc_attr(__('Original price', 'fluent-cart')); ?>">
1964 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1965 + <del>
1844 1966 <span><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></span></del>
1845 1967 </div>
1846 1968 <?php endif; ?>
1847 - <div class="fct-product-variant-item-price"
1848 - aria-label="<?php echo esc_attr(__('Current price', 'fluent-cart')); ?>">
1969 + <div class="fct-product-variant-item-price">
1970 + <span class="fct-sr-only"><?php echo $comparePrice ? esc_html__('Sale price:', 'fluent-cart') : esc_html__('Price:', 'fluent-cart'); ?></span>
1849 1971 <span><?php echo esc_html(Helper::toDecimal($itemPrice)); ?></span>
1850 1972 </div>
1851 1973 </div>
1852 1974 <?php endif; ?>
@@ -1866,9 +1988,9 @@
1866 1988 public function renderVariantImage($variant)
1867 1989 {
1868 1990 $image = $variant->thumbnail;
1869 1991 if (!$image) {
1870 - $image = Vite::getAssetUrl('images/placeholder.svg');
1992 + $image = Helper::getProductPlaceholderUrl();
1871 1993 }
1872 1994 ?>
1873 1995 <div class="fct-product-variant-image">
1874 1996 <img role="img" alt="<?php echo esc_attr($variant->variation_title); ?>"
@@ -1970,21 +2092,21 @@
1970 2092 //Convert to collection safely before sorting
1971 2093 $variants = (new Collection($variants))->sortBy('serial_index')->values();
1972 2094
1973 2095 foreach ($variants as $variant) {
1974 - do_action('fluent_cart/product/single/before_variant_item', [
2096 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1975 2097 'product' => $this->product,
1976 2098 'variant' => $variant,
1977 2099 'scope' => 'product_variant_item'
1978 - ]);
2100 + ]));
1979 2101
1980 2102 $this->renderVariationItem($variant, $this->defaultVariationId);
1981 2103
1982 - do_action('fluent_cart/product/single/after_variant_item', [
2104 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1983 2105 'product' => $this->product,
1984 2106 'variant' => $variant,
1985 2107 'scope' => 'product_variant_item'
1986 - ]);
2108 + ]));
1987 2109 }
1988 2110 ?>
1989 2111 </div>
1990 2112