| @@ -39,8 +39,16 @@ | ||
| 39 | 39 | private $cartSummaryRenderer; |
| 40 | 40 | |
| 41 | 41 | private $customer; |
| 42 | 42 | |
| 43 | + /** | |
| 44 | + * Memoised result of the before_payment_methods placement filter. Null until | |
| 45 | + * resolved; see beforePaymentMethodsPlacement() for why it is asked only once. | |
| 46 | + * | |
| 47 | + * @var string|null | |
| 48 | + */ | |
| 49 | + private $paymentMethodsHookPlacement = null; | |
| 50 | + | |
| 43 | 51 | public function __construct(Cart $cart, $config = []) |
| 44 | 52 | { |
| 45 | 53 | $this->cart = $cart; |
| 46 | 54 | $this->config = $config; |
| @@ -183,8 +191,40 @@ | ||
| 183 | 191 | // No explicit save — the recalc action that follows persists this |
| 184 | 192 | // together with the newly computed tax data, avoiding a double write. |
| 185 | 193 | } |
| 186 | 194 | |
| 195 | + /** | |
| 196 | + * Where `fluent_cart/before_payment_methods` fires in the modal. | |
| 197 | + * | |
| 198 | + * 'payment' (default) wraps the payment-method list, matching CheckoutRenderer. | |
| 199 | + * 'details' restores the pre-fix position in the address/shipping pane, for a | |
| 200 | + * store whose add-on positioned itself around that spot. | |
| 201 | + * | |
| 202 | + * Resolved ONCE per renderer and memoised. The two call sites are mutually | |
| 203 | + * exclusive, so the hook must fire exactly once — but that only holds if both | |
| 204 | + * sites agree, and a filter is free to be stateful or context-sensitive. Asking | |
| 205 | + * it twice would let it answer differently and either fire the hook twice or | |
| 206 | + * swallow it entirely. Ask once, remember the answer. | |
| 207 | + * | |
| 208 | + * @return string 'payment'|'details' | |
| 209 | + */ | |
| 210 | + private function beforePaymentMethodsPlacement(): string | |
| 211 | + { | |
| 212 | + if ($this->paymentMethodsHookPlacement !== null) { | |
| 213 | + return $this->paymentMethodsHookPlacement; | |
| 214 | + } | |
| 215 | + | |
| 216 | + $placement = apply_filters('fluent_cart/modal_checkout/before_payment_methods_placement', 'payment', [ | |
| 217 | + 'cart' => $this->cart, | |
| 218 | + ]); | |
| 219 | + | |
| 220 | + // Anything unrecognised falls back to the correct placement rather than | |
| 221 | + // silently dropping the hook. | |
| 222 | + $this->paymentMethodsHookPlacement = $placement === 'details' ? 'details' : 'payment'; | |
| 223 | + | |
| 224 | + return $this->paymentMethodsHookPlacement; | |
| 225 | + } | |
| 226 | + | |
| 187 | 227 | public function renderCheckoutDetails() |
| 188 | 228 | { |
| 189 | 229 | ?> |
| 190 | 230 | <div class="fct-modal-checkout-details"> |
| @@ -199,10 +239,28 @@ | ||
| 199 | 239 | <div class="fct_checkout_shipping_methods <?php echo $this->requireShipping ? '' : 'is-hidden' ?>"> |
| 200 | 240 | <?php $this->checkoutRenderer->renderShippingOptions(); ?> |
| 201 | 241 | </div> |
| 202 | 242 | |
| 203 | - <?php do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]); ?> | |
| 243 | + <?php | |
| 244 | + /** | |
| 245 | + * The position `fluent_cart/before_payment_methods` used to | |
| 246 | + * occupy in the modal. Given its own name so an add-on that | |
| 247 | + * genuinely wants this pane — under shipping, above the terms | |
| 248 | + * — has a stable place to render, instead of relying on a hook | |
| 249 | + * whose name promises it sits before the payment methods. | |
| 250 | + * | |
| 251 | + * @param array $data ['cart' => Cart] | |
| 252 | + */ | |
| 253 | + do_action('fluent_cart/modal_checkout/after_shipping_methods', ['cart' => $this->cart]); | |
| 204 | 254 | |
| 255 | + // Opt-in restoration of the old placement, for a store whose | |
| 256 | + // add-on positioned itself around the previous spot. Mutually | |
| 257 | + // exclusive with the call site in renderCheckoutBilling(). | |
| 258 | + if ($this->beforePaymentMethodsPlacement() === 'details') { | |
| 259 | + do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]); | |
| 260 | + } | |
| 261 | + ?> | |
| 262 | + | |
| 205 | 263 | <?php $this->checkoutRenderer->agreeTerms(); ?> |
| 206 | 264 | |
| 207 | 265 | <?php $this->renderSummaryGroup(); ?> |
| 208 | 266 | |
| @@ -564,12 +622,35 @@ | ||
| 564 | 622 | <?php echo esc_html__('Billing information', 'fluent-cart');?> |
| 565 | 623 | </h4> |
| 566 | 624 | </header> |
| 567 | 625 | |
| 626 | + <?php | |
| 627 | + /** | |
| 628 | + * Fired here, wrapping the payment methods, exactly as | |
| 629 | + * CheckoutRenderer does. It used to fire from | |
| 630 | + * renderCheckoutDetails() — the address/shipping pane — which | |
| 631 | + * is a different part of the modal entirely, so anything | |
| 632 | + * hooked to it (the saved-payment-method picker, Turnstile) | |
| 633 | + * rendered detached from the methods it belongs to. A hook | |
| 634 | + * named "before payment methods" has to fire before the | |
| 635 | + * payment methods. | |
| 636 | + */ | |
| 637 | + if ($this->beforePaymentMethodsPlacement() === 'payment') { | |
| 638 | + do_action('fluent_cart/before_payment_methods', ['cart' => $this->cart]); | |
| 639 | + } | |
| 640 | + ?> | |
| 641 | + | |
| 568 | 642 | <div class="fct_checkout_payment_methods" data-fluent-cart-checkout-payment-methods> |
| 569 | 643 | <?php $this->renderPaymentMethods(); ?> |
| 570 | 644 | </div> |
| 571 | 645 | |
| 646 | + <?php | |
| 647 | + // The modal never fired this at all, so add-ons rendering | |
| 648 | + // below the methods (the save-my-card consent box) were | |
| 649 | + // simply absent from quick checkout. | |
| 650 | + do_action('fluent_cart/after_payment_methods', ['cart' => $this->cart]); | |
| 651 | + ?> | |
| 652 | + | |
| 572 | 653 | <div class="fct-modal-checkout-btn-wrap"> |
| 573 | 654 | <?php (new CheckoutRenderer($this->cart))->renderCheckoutButton(); ?> |
| 574 | 655 | </div> |
| 575 | 656 | </div> |
| @@ -608,8 +689,23 @@ | ||
| 608 | 689 | $firstMethod = reset($activePaymentMethods); |
| 609 | 690 | $selectedPaymentMethod = $firstMethod ? $firstMethod->getMeta('route') : ''; |
| 610 | 691 | } |
| 611 | 692 | |
| 693 | + /** | |
| 694 | + * The payment method rendered as checked. An add-on that renders its own | |
| 695 | + * selector outside this list (e.g. saved payment methods) returns a route | |
| 696 | + * that is not in the list, so no gateway here is checked — the page then | |
| 697 | + * arrives with the add-on's choice already selected instead of a gateway | |
| 698 | + * being checked first and switched over by JS after load. | |
| 699 | + * | |
| 700 | + * @param string $selectedPaymentMethod | |
| 701 | + * @param array $context ['cart' => Cart, 'payment_methods' => array] | |
| 702 | + */ | |
| 703 | + $selectedPaymentMethod = (string) apply_filters('fluent_cart/checkout/selected_payment_method', $selectedPaymentMethod, [ | |
| 704 | + 'cart' => $this->cart, | |
| 705 | + 'payment_methods' => $activePaymentMethods | |
| 706 | + ]); | |
| 707 | + | |
| 612 | 708 | $checkoutMethodStyle = $this->storeSettings->get('checkout_method_style', 'logo'); |
| 613 | 709 | |
| 614 | 710 | ?> |
| 615 | 711 | <div id="fluent_payment_methods" class="fct_modal_payment_methods fluent_payment_methods"> |
| @@ -720,9 +816,9 @@ | ||
| 720 | 816 | 'route' => $route, |
| 721 | 817 | 'method_title' => $methodTitle, |
| 722 | 818 | 'method_style' => $methodStyle, |
| 723 | 819 | ]; |
| 724 | - $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.'); | |
| 820 | + $paymentMethodClass = ''; | |
| 725 | 821 | $paymentMethodClass = apply_filters('fluent_cart/payment_method_list_class', $paymentMethodClass, $pmContext); |
| 726 | 822 | |
| 727 | 823 | ?> |
| 728 | 824 | <div class="fluent-cart-checkout_embed_payment_wrapper"> |