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 +194 -73 1.6.0 → 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 {
@@ -907,14 +983,22 @@
907 983 $excerpt = $this->product->post_excerpt;
908 984 if (!$excerpt) {
909 985 return;
910 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);
911 995 ?>
912 996 <div class="fct-product-excerpt" aria-labelledby="fct-product-summary-title">
913 997 <p><?php echo wp_kses_post($excerpt); ?></p>
914 998 </div>
915 999 <?php
916 -
1000 + do_action('fluent_cart/product/single/after_excerpt_block', $gateContext);
917 1001 }
918 1002
919 1003 public function renderDescription()
920 1004 {
@@ -944,8 +1028,14 @@
944 1028 }
945 1029
946 1030 public function renderPrices()
947 1031 {
1032 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1033 +
1034 + if (!RenderGate::shouldRender('price', $gateContext)) {
1035 + return;
1036 + }
1037 +
948 1038 if ($this->product->detail->variation_type === 'simple') {
949 1039 // we have to render for the simple product
950 1040
951 1041 $first_price = $this->product->variants()->first();
@@ -959,97 +1049,76 @@
959 1049 $comparePrice = $first_price ? (int)$first_price->compare_price : 0;
960 1050 if ($comparePrice <= $itemPrice) {
961 1051 $comparePrice = 0;
962 1052 }
963 - do_action('fluent_cart/product/single/before_price_block', [
1053 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
964 1054 'product' => $this->product,
965 1055 'current_price' => $itemPrice,
966 1056 'scope' => 'price_range'
967 - ]);
1057 + ]));
968 1058 ?>
969 - <?php
1059 + <div class="fct-price-range fct-product-prices">
970 1060
971 - if ($comparePrice) {
972 - $aria_label = sprintf(
973 - /* translators: 1: Original price, 2: Current item price */
974 - __('Original Price: %1$s, Price: %2$s', 'fluent-cart'),
975 - Helper::toDecimal($comparePrice),
976 - Helper::toDecimal($itemPrice)
977 - );
978 - } else {
979 - $aria_label = sprintf(
980 - /* translators: 1: Current item price */
981 - __('Price: %1$s', 'fluent-cart'),
982 - Helper::toDecimal($itemPrice)
983 - );
984 - }
985 -
986 - ?>
987 - <div class="fct-price-range fct-product-prices" role="term"
988 - aria-label="<?php echo esc_attr($aria_label); ?>">
989 -
990 1061 <?php if ($comparePrice): ?>
991 1062 <span class="fct-compare-price">
992 - <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>
993 1065 </span>
994 1066 <?php endif; ?>
995 - <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>
996 1069 <?php echo esc_html(Helper::toDecimal($itemPrice)); ?>
997 - <?php do_action('fluent_cart/product/after_price', [
1070 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
998 1071 'product' => $this->product,
1072 + 'variant' => $first_price,
999 1073 'current_price' => $itemPrice,
1000 1074 'scope' => 'price_range'
1001 - ]); ?>
1075 + ])); ?>
1076 + <?php RenderHelper::renderPriceSuffix($this->product, $first_price, 'price_range'); ?>
1002 1077 </span>
1003 1078 </div>
1004 1079 <?php
1005 - do_action('fluent_cart/product/single/after_price_block', [
1080 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
1006 1081 'product' => $this->product,
1007 1082 'current_price' => $itemPrice,
1008 1083 'scope' => 'price_range'
1009 - ]);
1084 + ]));
1010 1085 return;
1011 1086 }
1012 1087 $min_price = $this->product->detail->min_price;
1013 1088 $max_price = $this->product->detail->max_price;
1014 1089
1015 - do_action('fluent_cart/product/single/before_price_range_block', [
1090 + do_action('fluent_cart/product/single/before_price_range_block', RenderContext::decorate([
1016 1091 'product' => $this->product,
1017 1092 'current_price' => $min_price,
1018 1093 'scope' => 'price_range'
1019 - ]);
1094 + ]));
1020 1095 ?>
1021 - <?php
1022 - $aria_label = sprintf(
1023 - /* translators: 1: Minimum price, 2: Maximum price */
1024 - __('Price range: %1$s - %2$s', 'fluent-cart'),
1025 - Helper::toDecimal($min_price),
1026 - Helper::toDecimal($max_price)
1027 - );
1028 - ?>
1029 - <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">
1030 1097
1031 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>
1032 1100 <span class="fct-min-price"><?php echo esc_html(Helper::toDecimal($min_price)); ?></span>
1033 1101 <span class="fct-price-separator" aria-hidden="true">-</span>
1102 + <span class="fct-sr-only"><?php echo esc_html__('to', 'fluent-cart'); ?></span>
1034 1103 <?php endif; ?>
1035 1104 <span class="fct-max-price">
1036 1105 <?php echo esc_html(Helper::toDecimal($max_price)); ?>
1037 1106 </span>
1038 1107
1039 - <?php do_action('fluent_cart/product/after_price', [
1108 + <?php do_action('fluent_cart/product/after_price', RenderContext::decorate([
1040 1109 'product' => $this->product,
1041 1110 'current_price' => $min_price,
1042 1111 'scope' => 'price_range'
1043 - ]); ?>
1112 + ])); ?>
1044 1113
1045 1114 </div>
1046 1115 <?php
1047 - do_action('fluent_cart/product/single/after_price_range_block', [
1116 + do_action('fluent_cart/product/single/after_price_range_block', RenderContext::decorate([
1048 1117 'product' => $this->product,
1049 1118 'current_price' => $min_price,
1050 1119 'scope' => 'price_range'
1051 - ]);
1120 + ]));
1052 1121 }
1053 1122
1054 1123 public function renderVariants($atts = [])
1055 1124 {
@@ -1074,19 +1143,19 @@
1074 1143 ?>
1075 1144 <div class="<?php echo esc_attr(implode(' ', $classes)); ?>" role="radiogroup"
1076 1145 aria-label="<?php esc_attr_e('Product Variants', 'fluent-cart'); ?>">
1077 1146 <?php foreach ($variants as $variant) {
1078 - do_action('fluent_cart/product/single/before_variant_item', [
1147 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1079 1148 'product' => $this->product,
1080 1149 'variant' => $variant,
1081 1150 'scope' => 'product_variant_item'
1082 - ]);
1151 + ]));
1083 1152 $this->renderVariationItem($variant, $this->defaultVariationId);
1084 - do_action('fluent_cart/product/single/after_variant_item', [
1153 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1085 1154 'product' => $this->product,
1086 1155 'variant' => $variant,
1087 1156 'scope' => 'product_variant_item'
1088 - ]);
1157 + ]));
1089 1158 } ?>
1090 1159 </div>
1091 1160 <?php
1092 1161 }
@@ -1092,17 +1161,24 @@
1092 1161 }
1093 1162
1094 1163 public function renderItemPrice()
1095 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 +
1096 1172 if ($this->product->detail->variation_type === 'simple' && !$this->hasSubscription) {
1097 1173 return; // for simple product we already rendered the price
1098 1174 }
1099 1175
1100 - do_action('fluent_cart/product/single/before_price_block', [
1176 + do_action('fluent_cart/product/single/before_price_block', RenderContext::decorate([
1101 1177 'product' => $this->product,
1102 1178 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1103 1179 'scope' => 'product_variant_price'
1104 - ]);
1180 + ]));
1105 1181
1106 1182 foreach ($this->product->variants as $variant) {
1107 1183 if ($this->shouldRenderPriceInPriceSection()) {
1108 1184 $this->renderVariantPricingWrapperStart($variant);
@@ -1136,13 +1212,13 @@
1136 1212
1137 1213
1138 1214
1139 1215
1140 - do_action('fluent_cart/product/single/after_price_block', [
1216 + do_action('fluent_cart/product/single/after_price_block', RenderContext::decorate([
1141 1217 'product' => $this->product,
1142 1218 'current_price' => $this->defaultVariant ? $this->defaultVariant->item_price : 0,
1143 1219 'scope' => 'product_variant_price'
1144 - ]);
1220 + ]));
1145 1221 }
1146 1222
1147 1223 public function shouldRenderPriceInPriceSection(): bool
1148 1224 {
@@ -1169,14 +1245,14 @@
1169 1245 'product' => $this->product,
1170 1246 'variant' => $variant,
1171 1247 'scope' => 'product_variant_price'
1172 1248 ]));
1173 - do_action('fluent_cart/product/after_price', [
1249 + do_action('fluent_cart/product/after_price', RenderContext::decorate([
1174 1250 'product' => $this->product,
1175 1251 'variant' => $variant,
1176 1252 'current_price' => $variant->item_price,
1177 1253 'scope' => 'product_variant_price'
1178 - ]);
1254 + ]));
1179 1255 RenderHelper::renderPriceSuffix($this->product, $variant, 'product_variant_price');
1180 1256 }
1181 1257
1182 1258 public function renderComparePriceWrapperStart($atts = [])
@@ -1199,8 +1275,9 @@
1199 1275 return;
1200 1276 } ?>
1201 1277
1202 1278 <span class="fct-compare-price">
1279 + <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
1203 1280 <del><?php echo esc_html(Helper::toDecimal($variant->compare_price)); ?></del>
1204 1281 </span>
1205 1282 <?php
1206 1283 }
@@ -1286,8 +1363,14 @@
1286 1363 <?php }
1287 1364
1288 1365 public function renderQuantity()
1289 1366 {
1367 + $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_SINGLE, $this->defaultVariant);
1368 +
1369 + if (!RenderGate::shouldRender('quantity', $gateContext)) {
1370 + return;
1371 + }
1372 +
1290 1373 $soldIndividually = $this->product->soldIndividually();
1291 1374
1292 1375 if (!$this->hasOnetime || $soldIndividually) {
1293 1376 return;
@@ -1306,12 +1389,12 @@
1306 1389 if ($this->hasSubscription && Arr::get($defaultVariantData, 'payment_type') !== 'onetime') {
1307 1390 $attributes['class'] .= ' is-hidden';
1308 1391 }
1309 1392
1310 - do_action('fluent_cart/product/single/before_quantity_block', [
1393 + do_action('fluent_cart/product/single/before_quantity_block', RenderContext::decorate([
1311 1394 'product' => $this->product,
1312 1395 'scope' => 'product_quantity_block'
1313 - ]);
1396 + ]));
1314 1397 ?>
1315 1398 <div <?php $this->renderAttributes($attributes); ?>>
1316 1399 <label for="fct-product-qty-input" class="quantity-title">
1317 1400 <?php esc_html_e('Quantity', 'fluent-cart'); ?>
@@ -1355,23 +1438,39 @@
1355 1438 </button>
1356 1439 </div>
1357 1440 </div>
1358 1441 <?php
1359 - do_action('fluent_cart/product/single/after_quantity_block', [
1442 + do_action('fluent_cart/product/single/after_quantity_block', RenderContext::decorate([
1360 1443 'product' => $this->product,
1361 1444 'scope' => 'product_quantity_block'
1362 - ]);
1445 + ]));
1363 1446 }
1364 1447
1365 1448 public function renderPurchaseButtons($atts = [])
1366 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 +
1367 1458 $buyNowButtonAtts = $atts;
1368 1459 $this->renderBuyNowButton($buyNowButtonAtts);
1369 1460 $this->renderAddToCartButton($atts);
1461 +
1462 + do_action('fluent_cart/product/single/after_actions_block', $gateContext);
1370 1463 }
1371 1464
1372 1465 public function renderBuyNowButton($atts = [])
1373 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 +
1374 1473 // Stock management check using isStock() method
1375 1474 // if (ModuleSettings::isActive('stock_management')) {
1376 1475 // if ($this->product->detail->variation_type === 'simple' && $this->defaultVariant) {
1377 1476 // if (!$this->defaultVariant->isStock()) {
@@ -1453,8 +1552,16 @@
1453 1552 }
1454 1553
1455 1554 public function renderBuyNowButtonBlock($atts = [])
1456 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 +
1457 1564 $text = Arr::get($atts, 'text', __('Buy Now', 'fluent-cart'));
1458 1565 $variantIds = Arr::get($atts, 'variant_ids', []);
1459 1566 $variantId = Arr::get($variantIds, 0);
1460 1567 $customClass = trim(Arr::get($atts, 'class', ''));
@@ -1539,8 +1646,14 @@
1539 1646 }
1540 1647
1541 1648 public function renderAddToCartButton($atts = [])
1542 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 +
1543 1656 $defaults = [
1544 1657 'buy_now_text' => __('Buy Now', 'fluent-cart'),
1545 1658 'add_to_cart_text' => __('Add To Cart', 'fluent-cart'),
1546 1659 ];
@@ -1635,8 +1748,15 @@
1635 1748 }
1636 1749
1637 1750 public function renderAddToCartButtonBlock($atts = [])
1638 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 +
1639 1759 $text = Arr::get($atts, 'text', __('Add To Cart', 'fluent-cart'));
1640 1760 $customClass = trim(Arr::get($atts, 'class', ''));
1641 1761 $extraClass = trim(Arr::get($atts, 'extra_class', ''));
1642 1762
@@ -1828,9 +1948,9 @@
1828 1948 }
1829 1949 ?>
1830 1950 <?php if ($this->viewType === 'both' || $this->viewType === 'text'): ?>
1831 1951 <div class="fct-product-variant-text">
1832 - <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>
1833 1953 <?php if (!$this->shouldRenderPriceInPriceSection() && $paymentType === 'subscription'): ?>
1834 1954 <?php $this->renderSubscriptionInfo($variant); ?>
1835 1955 <?php endif; ?>
1836 1956 </div>
@@ -1840,14 +1960,15 @@
1840 1960 <?php if (!$this->shouldRenderPriceInPriceSection()): ?>
1841 1961 <div class="fct-product-variant-price">
1842 1962 <?php if ($comparePrice): ?>
1843 1963 <div class="fct-product-variant-compare-price">
1844 - <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>
1845 1966 <span><?php echo esc_html(Helper::toDecimal($comparePrice)); ?></span></del>
1846 1967 </div>
1847 1968 <?php endif; ?>
1848 - <div class="fct-product-variant-item-price"
1849 - 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>
1850 1971 <span><?php echo esc_html(Helper::toDecimal($itemPrice)); ?></span>
1851 1972 </div>
1852 1973 </div>
1853 1974 <?php endif; ?>
@@ -1867,9 +1988,9 @@
1867 1988 public function renderVariantImage($variant)
1868 1989 {
1869 1990 $image = $variant->thumbnail;
1870 1991 if (!$image) {
1871 - $image = Vite::getAssetUrl('images/placeholder.svg');
1992 + $image = Helper::getProductPlaceholderUrl();
1872 1993 }
1873 1994 ?>
1874 1995 <div class="fct-product-variant-image">
1875 1996 <img role="img" alt="<?php echo esc_attr($variant->variation_title); ?>"
@@ -1971,21 +2092,21 @@
1971 2092 //Convert to collection safely before sorting
1972 2093 $variants = (new Collection($variants))->sortBy('serial_index')->values();
1973 2094
1974 2095 foreach ($variants as $variant) {
1975 - do_action('fluent_cart/product/single/before_variant_item', [
2096 + do_action('fluent_cart/product/single/before_variant_item', RenderContext::decorate([
1976 2097 'product' => $this->product,
1977 2098 'variant' => $variant,
1978 2099 'scope' => 'product_variant_item'
1979 - ]);
2100 + ]));
1980 2101
1981 2102 $this->renderVariationItem($variant, $this->defaultVariationId);
1982 2103
1983 - do_action('fluent_cart/product/single/after_variant_item', [
2104 + do_action('fluent_cart/product/single/after_variant_item', RenderContext::decorate([
1984 2105 'product' => $this->product,
1985 2106 'variant' => $variant,
1986 2107 'scope' => 'product_variant_item'
1987 - ]);
2108 + ]));
1988 2109 }
1989 2110 ?>
1990 2111 </div>
1991 2112