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 +205 -82 1.5.4 → 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 {
@@ -821,23 +898,24 @@
821 898 );
822 899 }
823 900 }
824 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 + */
825 907 public function renderPackageDescription($wrapper_attributes = '', $showName = true, $showDimensions = true, $showProductWeight = true, $showTotalWeight = true, $variant = null)
826 908 {
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 - }
909 + (new PackageDescriptionRenderer($this->product))->renderPackageDescription(
910 + $wrapper_attributes,
911 + $showName,
912 + $showDimensions,
913 + $showProductWeight,
914 + $showTotalWeight,
915 + $variant,
916 + $this->defaultVariant
917 + );
840 918 }
841 919
842 920 /**
843 921 * Build a JSON string of package info for a variant (used as data attribute for JS switching).
@@ -906,14 +984,22 @@
906 984 $excerpt = $this->product->post_excerpt;
907 985 if (!$excerpt) {
908 986 return;
909 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);
910 996 ?>
911 997 <div class="fct-product-excerpt" aria-labelledby="fct-product-summary-title">
912 998 <p><?php echo wp_kses_post($excerpt); ?></p>
913 999 </div>
914 1000 <?php
915 -
1001 + do_action('fluent_cart/product/single/after_excerpt_block', $gateContext);
916 1002 }
917 1003
918 1004 public function renderDescription()
919 1005 {
@@ -943,8 +1029,14 @@
943 1029 }
944 1030
945 1031 public function renderPrices()
946 1032 {
1033 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1034 +
1035 + if (!RenderGate::shouldRender('price', $gateContext)) {
1036 + return;
1037 + }
1038 +
947 1039 if ($this->product->detail->variation_type === 'simple') {
948 1040 // we have to render for the simple product
949 1041
950 1042 $first_price = $this->product->variants()->first();
@@ -958,97 +1050,76 @@
958 1050 $comparePrice = $first_price ? (int)$first_price->compare_price : 0;
959 1051 if ($comparePrice <= $itemPrice) {
960 1052 $comparePrice = 0;
961 1053 }
962 - do_action('fluent_cart/product/single/before_price_block', [
1054 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
963 1055 'product' => $this->product,
964 1056 'current_price' => $itemPrice,
965 1057 'scope' => 'price_range'
966 - ]);
1058 + ]));
967 1059 ?>
968 - <?php
1060 + <div class="fct-price-range fct-product-prices">
969 1061
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 1062 <?php if ($comparePrice): ?>
990 1063 <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>
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>
992 1066 </span>
993 1067 <?php endif; ?>
994 - <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>
995 1070 <?php echo esc_html(Helper::toDecimal($itemPrice)); ?>
996 - <?php do_action('fluent_cart/product/after_price', [
1071 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
997 1072 'product' => $this->product,
1073 + 'variant' => $first_price,
998 1074 'current_price' => $itemPrice,
999 1075 'scope' => 'price_range'
1000 - ]); ?>
1076 + ])); ?>
1077 + <?php RenderHelper::renderPriceSuffix($this->product, $first_price, 'price_range'); ?>
1001 1078 </span>
1002 1079 </div>
1003 1080 <?php
1004 - do_action('fluent_cart/product/single/after_price_block', [
1081 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
1005 1082 'product' => $this->product,
1006 1083 'current_price' => $itemPrice,
1007 1084 'scope' => 'price_range'
1008 - ]);
1085 + ]));
1009 1086 return;
1010 1087 }
1011 1088 $min_price = $this->product->detail->min_price;
1012 1089 $max_price = $this->product->detail->max_price;
1013 1090
1014 - do_action('fluent_cart/product/single/before_price_range_block', [
1091 + do_action('fluent_cart/product/single/before_price_range_block', RenderContext::decorate([
1015 1092 'product' => $this->product,
1016 1093 'current_price' => $min_price,
1017 1094 'scope' => 'price_range'
1018 - ]);
1095 + ]));
1019 1096 ?>
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); ?>">
1097 + <div class="fct-product-prices fct-price-range">
1029 1098
1030 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>
1031 1101 <span class="fct-min-price"><?php echo esc_html(Helper::toDecimal($min_price)); ?></span>
1032 1102 <span class="fct-price-separator" aria-hidden="true">-</span>
1103 + <span class="fct-sr-only"><?php echo esc_html__('to', 'fluent-cart'); ?></span>
1033 1104 <?php endif; ?>
1034 1105 <span class="fct-max-price">
1035 1106 <?php echo esc_html(Helper::toDecimal($max_price)); ?>
1036 1107 </span>
1037 1108
1038 - <?php do_action('fluent_cart/product/after_price', [
1109 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
1039 1110 'product' => $this->product,
1040 1111 'current_price' => $min_price,
1041 1112 'scope' => 'price_range'
1042 - ]); ?>
1113 + ])); ?>
1043 1114
1044 1115 </div>
1045 1116 <?php
1046 - do_action('fluent_cart/product/single/after_price_range_block', [
1117 + do_action('fluent_cart/product/single/after_price_range_block', RenderContext::decorate([
1047 1118 'product' => $this->product,
1048 1119 'current_price' => $min_price,
1049 1120 'scope' => 'price_range'
1050 - ]);
1121 + ]));
1051 1122 }
1052 1123
1053 1124 public function renderVariants($atts = [])
1054 1125 {
@@ -1073,19 +1144,19 @@
1073 1144 ?>
1074 1145 <div class="<?php echo esc_attr(implode(' ', $classes)); ?>" role="radiogroup"
1075 1146 aria-label="<?php esc_attr_e('Product Variants', 'fluent-cart'); ?>">
1076 1147 <?php foreach ($variants as $variant) {
1077 - do_action('fluent_cart/product/single/before_variant_item', [
1148 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1078 1149 'product' => $this->product,
1079 1150 'variant' => $variant,
1080 1151 'scope' => 'product_variant_item'
1081 - ]);
1152 + ]));
1082 1153 $this->renderVariationItem($variant, $this->defaultVariationId);
1083 - do_action('fluent_cart/product/single/after_variant_item', [
1154 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1084 1155 'product' => $this->product,
1085 1156 'variant' => $variant,
1086 1157 'scope' => 'product_variant_item'
1087 - ]);
1158 + ]));
1088 1159 } ?>
1089 1160 </div>
1090 1161 <?php
1091 1162 }
@@ -1091,17 +1162,24 @@
1091 1162 }
1092 1163
1093 1164 public function renderItemPrice()
1094 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 +
1095 1173 if ($this->product->detail->variation_type === 'simple' && !$this->hasSubscription) {
1096 1174 return; // for simple product we already rendered the price
1097 1175 }
1098 1176
1099 - do_action('fluent_cart/product/single/before_price_block', [
1177 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
1100 1178 'product' => $this->product,
1101 1179 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1102 1180 'scope' => 'product_variant_price'
1103 - ]);
1181 + ]));
1104 1182
1105 1183 foreach ($this->product->variants as $variant) {
1106 1184 if ($this->shouldRenderPriceInPriceSection()) {
1107 1185 $this->renderVariantPricingWrapperStart($variant);
@@ -1135,13 +1213,13 @@
1135 1213
1136 1214
1137 1215
1138 1216
1139 - do_action('fluent_cart/product/single/after_price_block', [
1217 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
1140 1218 'product' => $this->product,
1141 1219 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1142 1220 'scope' => 'product_variant_price'
1143 - ]);
1221 + ]));
1144 1222 }
1145 1223
1146 1224 public function shouldRenderPriceInPriceSection(): bool
1147 1225 {
@@ -1168,14 +1246,14 @@
1168 1246 'product' => $this->product,
1169 1247 'variant' => $variant,
1170 1248 'scope' => 'product_variant_price'
1171 1249 ]));
1172 - do_action('fluent_cart/product/after_price', [
1250 + do_action('fluent_cart/product/after_price', RenderContext::decorate([
1173 1251 'product' => $this->product,
1174 1252 'variant' => $variant,
1175 1253 'current_price' => $variant->item_price,
1176 1254 'scope' => 'product_variant_price'
1177 - ]);
1255 + ]));
1178 1256 RenderHelper::renderPriceSuffix($this->product, $variant, 'product_variant_price');
1179 1257 }
1180 1258
1181 1259 public function renderComparePriceWrapperStart($atts = [])
@@ -1198,8 +1276,9 @@
1198 1276 return;
1199 1277 } ?>
1200 1278
1201 1279 <span class="fct-compare-price">
1280 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1202 1281 <del><?php echo esc_html(Helper::toDecimal($variant->compare_price)); ?></del>
1203 1282 </span>
1204 1283 <?php
1205 1284 }
@@ -1285,8 +1364,14 @@
1285 1364 <?php }
1286 1365
1287 1366 public function renderQuantity()
1288 1367 {
1368 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1369 +
1370 + if (!RenderGate::shouldRender('quantity', $gateContext)) {
1371 + return;
1372 + }
1373 +
1289 1374 $soldIndividually = $this->product->soldIndividually();
1290 1375
1291 1376 if (!$this->hasOnetime || $soldIndividually) {
1292 1377 return;
@@ -1305,12 +1390,12 @@
1305 1390 if ($this->hasSubscription && Arr::get($defaultVariantData, 'payment_type') !== 'onetime') {
1306 1391 $attributes['class'] .= ' is-hidden';
1307 1392 }
1308 1393
1309 - do_action('fluent_cart/product/single/before_quantity_block', [
1394 + do_action('fluent_cart/product/single/before_quantity_block', RenderContext::decorate([
1310 1395 'product' => $this->product,
1311 1396 'scope' => 'product_quantity_block'
1312 - ]);
1397 + ]));
1313 1398 ?>
1314 1399 <div <?php $this->renderAttributes($attributes); ?>>
1315 1400 <label for="fct-product-qty-input" class="quantity-title">
1316 1401 <?php esc_html_e('Quantity', 'fluent-cart'); ?>
@@ -1354,23 +1439,39 @@
1354 1439 </button>
1355 1440 </div>
1356 1441 </div>
1357 1442 <?php
1358 - do_action('fluent_cart/product/single/after_quantity_block', [
1443 + do_action('fluent_cart/product/single/after_quantity_block', RenderContext::decorate([
1359 1444 'product' => $this->product,
1360 1445 'scope' => 'product_quantity_block'
1361 - ]);
1446 + ]));
1362 1447 }
1363 1448
1364 1449 public function renderPurchaseButtons($atts = [])
1365 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 +
1366 1459 $buyNowButtonAtts = $atts;
1367 1460 $this->renderBuyNowButton($buyNowButtonAtts);
1368 1461 $this->renderAddToCartButton($atts);
1462 +
1463 + do_action('fluent_cart/product/single/after_actions_block', $gateContext);
1369 1464 }
1370 1465
1371 1466 public function renderBuyNowButton($atts = [])
1372 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 +
1373 1474 // Stock management check using isStock() method
1374 1475 // if (ModuleSettings::isActive('stock_management')) {
1375 1476 // if ($this->product->detail->variation_type === 'simple' && $this->defaultVariant) {
1376 1477 // if (!$this->defaultVariant->isStock()) {
@@ -1452,8 +1553,16 @@
1452 1553 }
1453 1554
1454 1555 public function renderBuyNowButtonBlock($atts = [])
1455 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 +
1456 1565 $text = Arr::get($atts, 'text', __('Buy Now', 'fluent-cart'));
1457 1566 $variantIds = Arr::get($atts, 'variant_ids', []);
1458 1567 $variantId = Arr::get($variantIds, 0);
1459 1568 $customClass = trim(Arr::get($atts, 'class', ''));
@@ -1538,8 +1647,14 @@
1538 1647 }
1539 1648
1540 1649 public function renderAddToCartButton($atts = [])
1541 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 +
1542 1657 $defaults = [
1543 1658 'buy_now_text' => __('Buy Now', 'fluent-cart'),
1544 1659 'add_to_cart_text' => __('Add To Cart', 'fluent-cart'),
1545 1660 ];
@@ -1634,8 +1749,15 @@
1634 1749 }
1635 1750
1636 1751 public function renderAddToCartButtonBlock($atts = [])
1637 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 +
1638 1760 $text = Arr::get($atts, 'text', __('Add To Cart', 'fluent-cart'));
1639 1761 $customClass = trim(Arr::get($atts, 'class', ''));
1640 1762 $extraClass = trim(Arr::get($atts, 'extra_class', ''));
1641 1763
@@ -1827,9 +1949,9 @@
1827 1949 }
1828 1950 ?>
1829 1951 <?php if ($this->viewType === 'both' || $this->viewType === 'text'): ?>
1830 1952 <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>
1953 + <div class="fct-product-variant-title"><?php echo esc_html($variant->variation_title); ?></div>
1832 1954 <?php if (!$this->shouldRenderPriceInPriceSection() && $paymentType === 'subscription'): ?>
1833 1955 <?php $this->renderSubscriptionInfo($variant); ?>
1834 1956 <?php endif; ?>
1835 1957 </div>
@@ -1839,14 +1961,15 @@
1839 1961 <?php if (!$this->shouldRenderPriceInPriceSection()): ?>
1840 1962 <div class="fct-product-variant-price">
1841 1963 <?php if ($comparePrice): ?>
1842 1964 <div class="fct-product-variant-compare-price">
1843 - <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>
1844 1967 <span><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></span></del>
1845 1968 </div>
1846 1969 <?php endif; ?>
1847 - <div class="fct-product-variant-item-price"
1848 - 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>
1849 1972 <span><?php echo esc_html(Helper::toDecimal($itemPrice)); ?></span>
1850 1973 </div>
1851 1974 </div>
1852 1975 <?php endif; ?>
@@ -1970,21 +2093,21 @@
1970 2093 //Convert to collection safely before sorting
1971 2094 $variants = (new Collection($variants))->sortBy('serial_index')->values();
1972 2095
1973 2096 foreach ($variants as $variant) {
1974 - do_action('fluent_cart/product/single/before_variant_item', [
2097 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1975 2098 'product' => $this->product,
1976 2099 'variant' => $variant,
1977 2100 'scope' => 'product_variant_item'
1978 - ]);
2101 + ]));
1979 2102
1980 2103 $this->renderVariationItem($variant, $this->defaultVariationId);
1981 2104
1982 - do_action('fluent_cart/product/single/after_variant_item', [
2105 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1983 2106 'product' => $this->product,
1984 2107 'variant' => $variant,
1985 2108 'scope' => 'product_variant_item'
1986 - ]);
2109 + ]));
1987 2110 }
1988 2111 ?>
1989 2112 </div>
1990 2113