PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 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 All 51 releases
← All changes | app/Services/Renderer/ModalCheckoutRenderer.php +187 -43 1.4.1 → 1.7.1 View file →
@@ -3,19 +3,12 @@
3 3 namespace FluentCart\App\Services\Renderer;
4 4
5 5 use FluentCart\Api\PaymentMethods;
6 6 use FluentCart\Api\Resource\CustomerResource;
7 -use FluentCart\Api\Resource\FrontendResource\CustomerAddressResource;
8 7 use FluentCart\Api\StoreSettings;
9 -use FluentCart\App\App;
10 -use FluentCart\App\Helpers\AddressHelper;
11 -use FluentCart\App\Helpers\CartHelper;
12 8 use FluentCart\App\Helpers\Helper;
13 9 use FluentCart\App\Models\Cart;
14 -use FluentCart\App\Models\ProductVariation;
15 -use FluentCart\App\Services\Localization\LocalizationManager;
16 10 use FluentCart\App\Services\URL;
17 -use FluentCart\App\Vite;
18 11 use FluentCart\Framework\Support\Arr;
19 12 use FluentCart\App\Models\ProductMeta;
20 13
21 14 class ModalCheckoutRenderer
@@ -39,8 +32,16 @@
39 32 private $cartSummaryRenderer;
40 33
41 34 private $customer;
42 35
36 + /**
37 + * Memoised result of the before_payment_methods placement filter. Null until
38 + * resolved; see beforePaymentMethodsPlacement() for why it is asked only once.
39 + *
40 + * @var string|null
41 + */
42 + private $paymentMethodsHookPlacement = null;
43 +
43 44 public function __construct(Cart $cart, $config = [])
44 45 {
45 46 $this->cart = $cart;
46 47 $this->config = $config;
@@ -183,8 +184,40 @@
183 184 // No explicit save — the recalc action that follows persists this
184 185 // together with the newly computed tax data, avoiding a double write.
185 186 }
186 187
188 + /**
189 + * Where `fluent_cart/before_payment_methods` fires in the modal.
190 + *
191 + * 'payment' (default) wraps the payment-method list, matching CheckoutRenderer.
192 + * 'details' restores the pre-fix position in the address/shipping pane, for a
193 + * store whose add-on positioned itself around that spot.
194 + *
195 + * Resolved ONCE per renderer and memoised. The two call sites are mutually
196 + * exclusive, so the hook must fire exactly once — but that only holds if both
197 + * sites agree, and a filter is free to be stateful or context-sensitive. Asking
198 + * it twice would let it answer differently and either fire the hook twice or
199 + * swallow it entirely. Ask once, remember the answer.
200 + *
201 + * @return string 'payment'|'details'
202 + */
203 + private function beforePaymentMethodsPlacement(): string
204 + {
205 + if ($this->paymentMethodsHookPlacement !== null) {
206 + return $this->paymentMethodsHookPlacement;
207 + }
208 +
209 + $placement = apply_filters('fluent_cart/modal_checkout/before_payment_methods_placement', 'payment', [
210 + 'cart' => $this->cart,
211 + ]);
212 +
213 + // Anything unrecognised falls back to the correct placement rather than
214 + // silently dropping the hook.
215 + $this->paymentMethodsHookPlacement = $placement === 'details' ? 'details' : 'payment';
216 +
217 + return $this->paymentMethodsHookPlacement;
218 + }
219 +
187 220 public function renderCheckoutDetails()
188 221 {
189 222 ?>
190 223 <div class="fct-modal-checkout-details">
@@ -199,10 +232,28 @@
199 232 <div class="fct_checkout_shipping_methods <?php echo $this->requireShipping ? '' : 'is-hidden' ?>">
200 233 <?php $this->checkoutRenderer->renderShippingOptions(); ?>
201 234 </div>
202 235
203 - <?php do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]); ?>
236 + <?php
237 + /**
238 + * The position `fluent_cart/before_payment_methods` used to
239 + * occupy in the modal. Given its own name so an add-on that
240 + * genuinely wants this pane — under shipping, above the terms
241 + * — has a stable place to render, instead of relying on a hook
242 + * whose name promises it sits before the payment methods.
243 + *
244 + * @param array $data ['cart' => Cart]
245 + */
246 + do_action('fluent_cart/modal_checkout/after_shipping_methods', ['cart' => $this->cart]);
204 247
248 + // Opt-in restoration of the old placement, for a store whose
249 + // add-on positioned itself around the previous spot. Mutually
250 + // exclusive with the call site in renderCheckoutBilling().
251 + if ($this->beforePaymentMethodsPlacement() === 'details') {
252 + do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]);
253 + }
254 + ?>
255 +
205 256 <?php $this->checkoutRenderer->agreeTerms(); ?>
206 257
207 258 <?php $this->renderSummaryGroup(); ?>
208 259
@@ -219,9 +270,9 @@
219 270 $couponDiscount = (int) Arr::get($this->cart->cart_data, '0.coupon_discount', 0);
220 271 $subtotalRaw = (int) Arr::get($this->cart->cart_data, '0.subtotal', 0);
221 272 $lineTotalRaw = (int) Arr::get($this->cart->cart_data, '0.line_total', 0);
222 273 $hasCouponDiscount = $couponDiscount > 0 && $lineTotalRaw < $subtotalRaw;
223 - $media = Arr::get($this->cart->cart_data, '0.featured_media', '');
274 + $media = Helper::productImageOrPlaceholder(Arr::get($this->cart->cart_data, '0.featured_media', ''));
224 275
225 276 // Mirror CartItemRenderer's event payload so the shared line-item hooks
226 277 // (e.g. the per-item tax breakdown) also fire in modal checkout.
227 278 $lineItemEventInfo = [
@@ -479,8 +530,58 @@
479 530
480 531 }
481 532
482 533
534 + /**
535 + * First/Last name inputs for the modal. Enabling either in Checkout Fields
536 + * replaces the single Full name input. The field definitions come from
537 + * CheckoutFieldsSchema, shared with the regular checkout; the modal only
538 + * moves the label out of the placeholder.
539 + *
540 + * First name is always rendered and required, because checkout validation
541 + * always asks for it in this mode. That keeps an older saved configuration
542 + * with only Last name enabled working.
543 + *
544 + * @return array[] field configs for FormFieldRenderer::renderField()
545 + */
546 + private function getFirstLastNameFields(): array
547 + {
548 + $schema = CheckoutFieldsSchema::getNameEmailFieldsSchema($this->cart);
549 + $fields = [];
550 +
551 + foreach (['billing_first_name', 'billing_last_name'] as $name) {
552 + $field = Arr::get($schema, 'fields.' . $name);
553 + if ($name === 'billing_first_name') {
554 + $field = wp_parse_args($field ?: [], [
555 + 'name' => $name,
556 + 'id' => $name,
557 + 'type' => 'text',
558 + 'data-type' => 'text',
559 + 'aria-label' => __('First Name', 'fluent-cart'),
560 + 'autocomplete' => 'given-name',
561 + 'value' => '',
562 + ]);
563 + $field['required'] = 'yes';
564 + }
565 +
566 + if (!$field) {
567 + continue;
568 + }
569 +
570 + $field['label'] = Arr::get($field, 'aria-label') . (Arr::get($field, 'required') ? ' *' : '');
571 + $field['placeholder'] = $name === 'billing_first_name' ? __('Jon', 'fluent-cart') : __('Doe', 'fluent-cart');
572 +
573 + $customerValue = $this->customer->{str_replace('billing_', '', $name)} ?? '';
574 + if ($customerValue) {
575 + $field['value'] = $customerValue;
576 + }
577 +
578 + $fields[] = $field;
579 + }
580 +
581 + return $fields;
582 + }
583 +
483 584 public function renderCheckoutBilling() {
484 585 $user = wp_get_current_user();
485 586 $formRender = new FormFieldRenderer();
486 587 $fullName = trim(
@@ -497,31 +598,44 @@
497 598 $emailField = Arr::get($fieldsSchema, 'basic_info.email', []);
498 599 $isRequiredFullName = Arr::get($fullNameField, 'required', 'no') === 'yes' ? 'yes' : '';
499 600 $isRequiredEmail = Arr::get($emailField, 'required', 'no') === 'yes' ? 'yes' : '';
500 601
602 + $isFullName = CheckoutFieldsSchema::isFullNameRequired();
603 + $firstLastNameFields = $isFullName ? [] : $this->getFirstLastNameFields();
604 + // First and last name share one row, so email gets the next row to itself.
605 + $emailColClass = count($firstLastNameFields) === 2 ? 'col-12' : 'col-6';
606 +
501 607 ?>
502 608 <div class="fct-modal-checkout-billing-wrap">
503 609 <!-- Account Details -->
504 610 <div class="fct-modal-account-details" data-fct-checkout-form-section>
505 611 <div class="fct-modal-form-info">
506 - <div class="fct-modal-form-field col-6">
612 + <?php if ($isFullName) : ?>
613 + <div class="fct-modal-form-field col-6">
614 + <?php
615 + $formRender->renderField([
616 + 'label' => esc_attr__('Full name', 'fluent-cart') . ($isRequiredFullName ? ' *' : ''),
617 + 'id' => 'billing_full_name',
618 + 'type' => 'text',
619 + 'placeholder' => __('Jon Doe', 'fluent-cart'),
620 + 'name' => 'billing_full_name',
621 + 'autocomplete' => 'given-name',
622 + 'aria-label' => esc_attr__('Full name', 'fluent-cart'),
623 + 'required' => $isRequiredFullName,
624 + 'value' => $fullName
625 + ]);
626 + ?>
627 + </div>
628 + <?php else : ?>
629 + <?php foreach ($firstLastNameFields as $nameField) : ?>
630 + <div class="fct-modal-form-field col-6">
631 + <?php $formRender->renderField($nameField); ?>
632 + </div>
633 + <?php endforeach; ?>
634 + <?php endif; ?>
635 + <div class="fct-modal-form-field <?php echo esc_attr($emailColClass); ?>">
507 636 <?php
508 637 $formRender->renderField([
509 - 'label' => esc_attr__('Full name', 'fluent-cart') . ($isRequiredFullName ? ' *' : ''),
510 - 'id' => 'billing_full_name',
511 - 'type' => 'text',
512 - 'placeholder' => __('Jon Doe', 'fluent-cart'),
513 - 'name' => 'billing_full_name',
514 - 'autocomplete' => 'given-name',
515 - 'aria-label' => esc_attr__('Full name', 'fluent-cart'),
516 - 'required' => $isRequiredFullName,
517 - 'value' => $fullName
518 - ]);
519 - ?>
520 - </div>
521 - <div class="fct-modal-form-field col-6">
522 - <?php
523 - $formRender->renderField([
524 638 'label' => esc_attr__('Email address', 'fluent-cart') . ($isRequiredEmail ? ' *' : ''),
525 639 'id' => 'billing_email',
526 640 'type' => 'text',
527 641 'placeholder' => '[email protected]',
@@ -533,23 +647,8 @@
533 647 'disabled' => (bool)($this->customer->email ?? $user->user_email)
534 648 ]);
535 649 ?>
536 650 </div>
537 -
538 -<!-- <div class="fct-modal-form-field col-3">-->
539 -<!-- --><?php
540 -// $formRender->renderField([
541 -// 'label' => __('Last name', 'fluent-cart'),
542 -// 'id' => 'billing_last_name',
543 -// 'type' => 'text',
544 -// 'placeholder' => __('Doe', 'fluent-cart'),
545 -// 'name' => 'billing_last_name',
546 -// 'autocomplete' => 'given-name',
547 -// 'required' => true,
548 -// 'value' => $this->customer->last_name ?? ''
549 -// ]);
550 -// ?>
551 -<!-- </div>-->
552 651 </div>
553 652
554 653 <?php $this->checkoutRenderer->renderCreateAccountField(); ?>
555 654
@@ -564,12 +663,35 @@
564 663 <?php echo esc_html__('Billing information', 'fluent-cart');?>
565 664 </h4>
566 665 </header>
567 666
667 + <?php
668 + /**
669 + * Fired here, wrapping the payment methods, exactly as
670 + * CheckoutRenderer does. It used to fire from
671 + * renderCheckoutDetails() — the address/shipping pane — which
672 + * is a different part of the modal entirely, so anything
673 + * hooked to it (the saved-payment-method picker, Turnstile)
674 + * rendered detached from the methods it belongs to. A hook
675 + * named "before payment methods" has to fire before the
676 + * payment methods.
677 + */
678 + if ($this->beforePaymentMethodsPlacement() === 'payment') {
679 + do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]);
680 + }
681 + ?>
682 +
568 683 <div class="fct_checkout_payment_methods" data-fluent-cart-checkout-payment-methods>
569 684 <?php $this->renderPaymentMethods(); ?>
570 685 </div>
571 686
687 + <?php
688 + // The modal never fired this at all, so add-ons rendering
689 + // below the methods (the save-my-card consent box) were
690 + // simply absent from quick checkout.
691 + do_action('fluent_cart/after_payment_methods', ['cart' => $this->cart]);
692 + ?>
693 +
572 694 <div class="fct-modal-checkout-btn-wrap">
573 695 <?php (new CheckoutRenderer($this->cart))->renderCheckoutButton(); ?>
574 696 </div>
575 697 </div>
@@ -588,8 +710,9 @@
588 710 }
589 711
590 712 $selectedPaymentMethod = Arr::get($this->cart->checkout_data, 'form_data._fct_pay_method', '');
591 713 $activePaymentMethods = PaymentMethods::getActiveMethodInstance($this->cart);
714 + $hadActiveMethods = !empty($activePaymentMethods);
592 715
593 716 $activePaymentMethods = apply_filters('fluent_cart/checkout_active_payment_methods', $activePaymentMethods, [
594 717 'cart' => $this->cart
595 718 ]);
@@ -602,11 +725,28 @@
602 725 });
603 726 }
604 727
605 728 if (!$selectedPaymentMethod && !empty($activePaymentMethods)) {
606 - $selectedPaymentMethod = $activePaymentMethods[0] ? $activePaymentMethods[0]->getMeta('route') : '';
729 + // reset() not [0] — array_filter above preserves keys, so key 0 may be gone.
730 + $firstMethod = reset($activePaymentMethods);
731 + $selectedPaymentMethod = $firstMethod ? $firstMethod->getMeta('route') : '';
607 732 }
608 733
734 + /**
735 + * The payment method rendered as checked. An add-on that renders its own
736 + * selector outside this list (e.g. saved payment methods) returns a route
737 + * that is not in the list, so no gateway here is checked — the page then
738 + * arrives with the add-on's choice already selected instead of a gateway
739 + * being checked first and switched over by JS after load.
740 + *
741 + * @param string $selectedPaymentMethod
742 + * @param array $context ['cart' => Cart, 'payment_methods' => array]
743 + */
744 + $selectedPaymentMethod = (string) apply_filters('fluent_cart/checkout/selected_payment_method', $selectedPaymentMethod, [
745 + 'cart' => $this->cart,
746 + 'payment_methods' => $activePaymentMethods
747 + ]);
748 +
609 749 $checkoutMethodStyle = $this->storeSettings->get('checkout_method_style', 'logo');
610 750
611 751 ?>
612 752 <div id="fluent_payment_methods" class="fct_modal_payment_methods fluent_payment_methods">
@@ -624,9 +764,13 @@
624 764 ]);
625 765 } ?>
626 766 <?php else: ?>
627 767 <?php
628 - $emptyText = esc_html__('No Payment method is activated for this site yet.', 'fluent-cart');
768 + if ($hadActiveMethods) {
769 + $emptyText = esc_html__('None of the available payment methods can process this order. Please contact the store.', 'fluent-cart');
770 + } else {
771 + $emptyText = esc_html__('No Payment method is activated for this site yet.', 'fluent-cart');
772 + }
629 773 if (current_user_can('manage_options')) {
630 774 $emptyText .= '<a href="' . esc_url(URL::getDashboardUrl('settings/payments')) . '" target="_blank">' . esc_html__('Activate from settings.', 'fluent-cart') . '</a>';
631 775 }
632 776 echo '<div class="fct-empty-state">' . wp_kses_post($emptyText) . '</div>';
@@ -713,9 +857,9 @@
713 857 'route' => $route,
714 858 'method_title' => $methodTitle,
715 859 'method_style' => $methodStyle,
716 860 ];
717 - $paymentMethodClass = apply_filters_deprecated('fluent_cart_payment_method_list_class', ['', $pmContext], '1.3.16', 'fluent_cart/payment_method_list_class', 'Use fluent_cart/payment_method_list_class instead of fluent_cart_payment_method_list_class.');
861 + $paymentMethodClass = '';
718 862 $paymentMethodClass = apply_filters('fluent_cart/payment_method_list_class', $paymentMethodClass, $pmContext);
719 863
720 864 ?>
721 865 <div class="fluent-cart-checkout_embed_payment_wrapper">