| @@ -3,8 +3,9 @@ | ||
| 3 | 3 | namespace Payplug\PayplugWoocommerce\Controller; |
| 4 | 4 | |
| 5 | 5 | use function is_cart; |
| 6 | 6 | use function is_product; |
| 7 | + | |
| 7 | 8 | use Payplug\Exception\HttpException; |
| 8 | 9 | use Payplug\PayplugWoocommerce\Gateway\PayplugAddressData; |
| 9 | 10 | use Payplug\PayplugWoocommerce\Gateway\PayplugGateway; |
| 10 | 11 | use Payplug\PayplugWoocommerce\PayplugWoocommerceHelper; |
| @@ -22,9 +23,9 @@ | ||
| 22 | 23 | protected $checkout = false; |
| 23 | 24 | |
| 24 | 25 | protected $carriers = []; |
| 25 | 26 | |
| 26 | - const ENABLE_ON_TEST_MODE = false; | |
| 27 | + public const ENABLE_ON_TEST_MODE = false; | |
| 27 | 28 | |
| 28 | 29 | public $image = 'apple-pay-checkout.svg'; |
| 29 | 30 | |
| 30 | 31 | protected $product = false; |
| @@ -42,9 +43,9 @@ | ||
| 42 | 43 | $this->has_fields = false; |
| 43 | 44 | |
| 44 | 45 | $this->title = __('payplug_apple_pay_title', 'payplug'); |
| 45 | 46 | $this->description = '<div id="apple-pay-button-wrapper"><apple-pay-button buttonstyle="black" type="pay" locale="' . get_locale() . '"></apple-pay-button></div>'; |
| 46 | - $this->domain_name = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : parse_url(home_url(), PHP_URL_HOST); | |
| 47 | + $this->domain_name = wp_parse_url(home_url(), PHP_URL_HOST); | |
| 47 | 48 | $this->enabled = 'no'; |
| 48 | 49 | |
| 49 | 50 | if ($this->checkApplePay() && is_admin()) { |
| 50 | 51 | $this->enabled = 'yes'; |
| @@ -53,11 +54,13 @@ | ||
| 53 | 54 | $this->enabled = 'yes'; |
| 54 | 55 | } |
| 55 | 56 | |
| 56 | 57 | if (!is_admin()) { |
| 57 | - if (!PayplugWoocommerceHelper::is_checkout_block() && $this->get_button_checkout()) { | |
| 58 | - $this->add_apple_pay_css(); | |
| 59 | - add_action('wp_enqueue_scripts', [$this, 'add_apple_pay_js']); | |
| 58 | + // Gateways are constructed before WordPress finishes parsing the request, so | |
| 59 | + // is_wc_endpoint_url('order-pay') can't be trusted yet here: defer that check | |
| 60 | + // to wp_enqueue_scripts, once routing has completed. | |
| 61 | + if ($this->get_button_checkout()) { | |
| 62 | + add_action('wp_enqueue_scripts', [$this, 'maybe_add_apple_pay_checkout_assets']); | |
| 60 | 63 | } |
| 61 | 64 | |
| 62 | 65 | if ($this->get_button_cart() && !PayplugWoocommerceHelper::is_cart_block() && !PayplugWoocommerceHelper::is_subscription()) { |
| 63 | 66 | $this->enabled = 'yes'; |
| @@ -160,9 +163,9 @@ | ||
| 160 | 163 | * Outputs the payment fields on the checkout page. |
| 161 | 164 | * |
| 162 | 165 | * @return void |
| 163 | 166 | */ |
| 164 | - public function payment_fields() | |
| 167 | + public function payment_fields(): void | |
| 165 | 168 | { |
| 166 | 169 | $description = $this->get_description(); |
| 167 | 170 | |
| 168 | 171 | if (!empty($description)) { |
| @@ -184,9 +187,9 @@ | ||
| 184 | 187 | * Enqueues Apple Pay scripts for the cart page. |
| 185 | 188 | * |
| 186 | 189 | * @return void |
| 187 | 190 | */ |
| 188 | - public function add_apple_pay_cart_js() | |
| 191 | + public function add_apple_pay_cart_js(): void | |
| 189 | 192 | { |
| 190 | 193 | wp_enqueue_script('apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js', [], false, true); |
| 191 | 194 | wp_enqueue_script('payplug-apple-pay-cart', PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/js/payplug-apple-pay-cart.js', ['jquery', 'apple-pay-sdk'], PAYPLUG_GATEWAY_VERSION, true); |
| 192 | 195 | wp_localize_script( |
| @@ -217,9 +220,9 @@ | ||
| 217 | 220 | * Enqueues Apple Pay scripts for the product page. |
| 218 | 221 | * |
| 219 | 222 | * @return void |
| 220 | 223 | */ |
| 221 | - public function add_apple_pay_product_js() | |
| 224 | + public function add_apple_pay_product_js(): void | |
| 222 | 225 | { |
| 223 | 226 | global $product; |
| 224 | 227 | // Only dispay ApplePay on product page for simple and variable products |
| 225 | 228 | if ($product->get_type() != 'simple' && $product->get_type() != 'variable') { |
| @@ -238,9 +241,9 @@ | ||
| 238 | 241 | 'is_virtual' => $product->is_virtual(), |
| 239 | 242 | 'cart_shipping' => WC()->cart->get_shipping_total(), |
| 240 | 243 | 'countryCode' => WC()->customer->get_billing_country(), |
| 241 | 244 | 'currencyCode' => get_woocommerce_currency(), |
| 242 | - 'apple_pay_domain' => $_SERVER['HTTP_HOST'], | |
| 245 | + 'apple_pay_domain' => $this->domain_name, | |
| 243 | 246 | ]; |
| 244 | 247 | wp_enqueue_script('apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js', [], false, true); |
| 245 | 248 | wp_enqueue_script('payplug-apple-pay-product', PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/js/payplug-apple-pay-product.js', ['jquery', 'apple-pay-sdk'], PAYPLUG_GATEWAY_VERSION, true); |
| 246 | 249 | wp_localize_script('payplug-apple-pay-product', 'apple_pay_params', $apple_pay_params); |
| @@ -274,10 +277,13 @@ | ||
| 274 | 277 | $available_rates = !empty($package['rates']) ? $package['rates'] : []; |
| 275 | 278 | if (!empty($available_rates)) { |
| 276 | 279 | foreach ($available_rates as $method) { |
| 277 | 280 | if (in_array($method->get_method_id(), $apple_carriers)) { |
| 278 | - if ($chosen_method === $method->get_method_id() . ':' . $method->get_instance_id()) { | |
| 279 | - $allowed = true; | |
| 281 | + // On cart page, show the button if any eligible carrier is available — | |
| 282 | + // the actual shipping selection happens inside the Apple Pay modal. | |
| 283 | + // On checkout, restrict to the currently chosen shipping method. | |
| 284 | + if (is_cart() || $chosen_method === $method->get_method_id() . ':' . $method->get_instance_id()) { | |
| 285 | + return true; | |
| 280 | 286 | } |
| 281 | 287 | } |
| 282 | 288 | } |
| 283 | 289 | } |
| @@ -316,9 +322,9 @@ | ||
| 316 | 322 | * Display unauthorized error |
| 317 | 323 | * |
| 318 | 324 | * @return void |
| 319 | 325 | */ |
| 320 | - public static function display_notice() | |
| 326 | + public static function display_notice(): void | |
| 321 | 327 | { |
| 322 | 328 | ?> |
| 323 | 329 | <div class="notice notice-error is-dismissible"> |
| 324 | 330 | <p><?php echo __('payplug_apple_pay_unauthorized_error', 'payplug'); ?></p> |
| @@ -352,19 +358,36 @@ | ||
| 352 | 358 | * Enqueues Apple Pay CSS styles. |
| 353 | 359 | * |
| 354 | 360 | * @return void |
| 355 | 361 | */ |
| 356 | - public function add_apple_pay_css() | |
| 362 | + public function add_apple_pay_css(): void | |
| 357 | 363 | { |
| 358 | 364 | wp_enqueue_style('payplug-apple-pay', PAYPLUG_GATEWAY_PLUGIN_URL . 'assets/css/payplug-apple-pay.css', [], PAYPLUG_GATEWAY_VERSION); |
| 359 | 365 | } |
| 360 | 366 | |
| 361 | 367 | /** |
| 368 | + * Enqueues the classic Apple Pay checkout assets, unless the checkout page uses the | |
| 369 | + * Cart & Checkout blocks (the order-pay page always renders the classic payment form, | |
| 370 | + * even then, so it still needs the classic assets). | |
| 371 | + * | |
| 372 | + * @return void | |
| 373 | + */ | |
| 374 | + public function maybe_add_apple_pay_checkout_assets(): void | |
| 375 | + { | |
| 376 | + if (PayplugWoocommerceHelper::is_checkout_block() && !is_wc_endpoint_url('order-pay')) { | |
| 377 | + return; | |
| 378 | + } | |
| 379 | + | |
| 380 | + $this->add_apple_pay_css(); | |
| 381 | + $this->add_apple_pay_js(); | |
| 382 | + } | |
| 383 | + | |
| 384 | + /** | |
| 362 | 385 | * Enqueues Apple Pay JavaScript for the checkout page. |
| 363 | 386 | * |
| 364 | 387 | * @return void |
| 365 | 388 | */ |
| 366 | - public function add_apple_pay_js() | |
| 389 | + public function add_apple_pay_js(): void | |
| 367 | 390 | { |
| 368 | 391 | wp_enqueue_script('apple-pay-sdk', 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js', [], false, true); |
| 369 | 392 | wp_enqueue_script( |
| 370 | 393 | 'payplug-apple-pay', |
| @@ -382,12 +405,17 @@ | ||
| 382 | 405 | [ |
| 383 | 406 | 'ajax_url_payplug_create_order' => \WC_AJAX::get_endpoint('payplug_create_order'), |
| 384 | 407 | 'ajax_url_applepay_update_payment' => \WC_AJAX::get_endpoint('applepay_update_payment'), |
| 385 | 408 | 'ajax_url_applepay_get_order_totals' => \WC_AJAX::get_endpoint('applepay_get_order_totals'), |
| 409 | + 'ajax_url_payplug_apple_pay_create_order_pay' => \WC_AJAX::get_endpoint('payplug_apple_pay_create_order_pay'), | |
| 386 | 410 | 'countryCode' => WC()->customer->get_billing_country(), |
| 387 | 411 | 'currencyCode' => get_woocommerce_currency(), |
| 388 | 412 | 'total' => WC()->cart->total, |
| 389 | 413 | 'is_checkout' => is_checkout(), |
| 414 | + 'is_order_pay' => is_wc_endpoint_url('order-pay'), | |
| 415 | + 'order_pay_id' => is_wc_endpoint_url('order-pay') ? (int) get_query_var('order-pay') : 0, | |
| 416 | + 'order_pay_key' => is_wc_endpoint_url('order-pay') ? wc_clean(wp_unslash($_GET['key'] ?? '')) : '', | |
| 417 | + 'wp_nonce' => wp_create_nonce('woocommerce-process_checkout'), | |
| 390 | 418 | 'apple_pay_domain' => $this->domain_name, |
| 391 | 419 | ] |
| 392 | 420 | ); |
| 393 | 421 | } |
| @@ -421,9 +449,15 @@ | ||
| 421 | 449 | * @return array|null |
| 422 | 450 | */ |
| 423 | 451 | private function process_standard_intent_payment($order) |
| 424 | 452 | { |
| 425 | - if (!is_wc_endpoint_url('order-pay') && | |
| 453 | + // This runs from the payplug_apple_pay_create_order_pay AJAX endpoint too, whose own | |
| 454 | + // request URL never carries the order-pay query var, so is_wc_endpoint_url() alone | |
| 455 | + // can't detect that context here: fall back to the order_key/order_pay_key sent by | |
| 456 | + // that endpoint and by the checkout-block create_payment_intent endpoint. | |
| 457 | + $is_order_pay = $this->is_order_pay_request($order); | |
| 458 | + | |
| 459 | + if (!$is_order_pay && | |
| 426 | 460 | PayplugWoocommerceHelper::is_checkout_block() && |
| 427 | 461 | !empty($order->get_transaction_id())) { |
| 428 | 462 | $order_id = PayplugWoocommerceHelper::is_pre_30() ? $order->id : $order->get_id(); |
| 429 | 463 | |
| @@ -433,19 +467,43 @@ | ||
| 433 | 467 | ob_clean(); |
| 434 | 468 | } |
| 435 | 469 | |
| 436 | 470 | $return_url = esc_url_raw($order->get_checkout_order_received_url()); |
| 471 | + $cancel_url = !empty($payment->hosted_payment->cancel_url) ? $payment->hosted_payment->cancel_url : esc_url_raw(wc_get_checkout_url()); | |
| 437 | 472 | |
| 438 | - wp_send_json_success( | |
| 439 | - [ | |
| 440 | - 'payment_id' => $payment->id, | |
| 441 | - 'result' => 'success', | |
| 442 | - 'redirect' => !empty($payment->hosted_payment->payment_url) ? $payment->hosted_payment->payment_url : $return_url, | |
| 443 | - 'cancel' => !empty($payment->hosted_payment->cancel_url) ? $payment->hosted_payment->cancel_url : null, | |
| 444 | - ] | |
| 445 | - ); | |
| 473 | + // Same payload shape as process_standard_payment(): the frontend's | |
| 474 | + // BeginSessionFromPaymentDetails() reads merchant_session/cancel_url/return_url | |
| 475 | + // off of it regardless of which of the two methods produced it. | |
| 476 | + // payment_method is a write-once attribute (merchant_session is tied to the | |
| 477 | + // ApplePaySession that created the payment): a retrieved payment may not carry | |
| 478 | + // it at all, and $payment->payment_method would throw UndefinedAttributeException | |
| 479 | + // rather than just being null/missing, unlike a plain array access. | |
| 480 | + $merchant_session = null; | |
| 481 | + if (isset($payment->payment_method) && is_array($payment->payment_method)) { | |
| 482 | + $merchant_session = $payment->payment_method['merchant_session'] ?? null; | |
| 483 | + } | |
| 446 | 484 | |
| 447 | - return ['stt' => 'OK']; | |
| 485 | + if (defined('REST_REQUEST') && REST_REQUEST) { | |
| 486 | + $merchant_session = wp_json_encode($merchant_session); | |
| 487 | + } | |
| 488 | + | |
| 489 | + $result = [ | |
| 490 | + 'result' => 'success', | |
| 491 | + 'merchant_session' => $merchant_session, | |
| 492 | + 'payment_id' => $payment->id, | |
| 493 | + 'cancel_url' => $cancel_url, | |
| 494 | + 'return_url' => $return_url, | |
| 495 | + ]; | |
| 496 | + | |
| 497 | + // wp_send_json_success() calls die(), which is only safe for the classic | |
| 498 | + // wc-ajax request this was written for: the Store API checkout flow (used by | |
| 499 | + // the checkout block) calls process_payment() through the REST framework, | |
| 500 | + // and killing the process mid-request there produces a broken response. | |
| 501 | + if (wp_doing_ajax()) { | |
| 502 | + wp_send_json_success($result); | |
| 503 | + } | |
| 504 | + | |
| 505 | + return $result; | |
| 448 | 506 | } catch (HttpException $e) { |
| 449 | 507 | PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error'); |
| 450 | 508 | throw new \Exception(__('Payment processing failed. Please retry.', 'payplug')); |
| 451 | 509 | } catch (\Exception $e) { |
| @@ -475,8 +533,12 @@ | ||
| 475 | 533 | if (!empty($intent)) { |
| 476 | 534 | return $intent; |
| 477 | 535 | } |
| 478 | 536 | |
| 537 | + // Same detection as process_standard_intent_payment(): this can run from AJAX | |
| 538 | + // endpoints whose own request URL never carries the order-pay query var. | |
| 539 | + $is_order_pay = $this->is_order_pay_request($order); | |
| 540 | + | |
| 479 | 541 | $order_id = PayplugWoocommerceHelper::is_pre_30() ? $order->id : $order->get_id(); |
| 480 | 542 | try { |
| 481 | 543 | $address_data = PayplugAddressData::from_order($order); |
| 482 | 544 | |
| @@ -557,13 +619,37 @@ | ||
| 557 | 619 | PayplugWoocommerceHelper::save_transaction_metadata($order, $metadata); |
| 558 | 620 | |
| 559 | 621 | PayplugGateway::log(sprintf('Payment creation complete for order #%s', $order_id)); |
| 560 | 622 | |
| 623 | + // On order-pay, closing the Apple Pay sheet should keep the customer on the | |
| 624 | + // order-pay page. On a regular checkout submission (classic or Blocks), it should | |
| 625 | + // keep them on checkout too, so they can pick another payment method - only the | |
| 626 | + // classic cart/product page flows (workflow 'cart'/'product') actually want the | |
| 627 | + // order-cancelled/cart redirect, since that's where those customers started. | |
| 628 | + if ($is_order_pay) { | |
| 629 | + $cancel_url = esc_url_raw($order->get_checkout_payment_url()); | |
| 630 | + } elseif ('checkout' === $workflow) { | |
| 631 | + $cancel_url = esc_url_raw(wc_get_checkout_url()); | |
| 632 | + } else { | |
| 633 | + $cancel_url = esc_url_raw($order->get_cancel_order_url_raw()); | |
| 634 | + } | |
| 635 | + | |
| 636 | + // When process_payment() is invoked through WooCommerce Blocks' Store API (a REST | |
| 637 | + // request), this array is forwarded to the client as `payment_details`, which | |
| 638 | + // coerces every value to a string - an array value would become the literal, | |
| 639 | + // useless string "Array". The classic AJAX flows that call this method directly | |
| 640 | + // (order-pay, cart/product Apple Pay) JSON-encode/decode the whole response | |
| 641 | + // transparently instead, so they need the raw merchant session object. | |
| 642 | + $merchant_session = $payment->payment_method['merchant_session']; | |
| 643 | + if (defined('REST_REQUEST') && REST_REQUEST) { | |
| 644 | + $merchant_session = wp_json_encode($merchant_session); | |
| 645 | + } | |
| 646 | + | |
| 561 | 647 | return [ |
| 562 | 648 | 'result' => 'success', |
| 563 | - 'merchant_session' => $payment->payment_method['merchant_session'], | |
| 649 | + 'merchant_session' => $merchant_session, | |
| 564 | 650 | 'payment_id' => $payment->id, |
| 565 | - 'cancel_url' => esc_url_raw($order->get_cancel_order_url_raw()), | |
| 651 | + 'cancel_url' => $cancel_url, | |
| 566 | 652 | 'return_url' => $return_url, |
| 567 | 653 | ]; |
| 568 | 654 | } catch (HttpException $e) { |
| 569 | 655 | PayplugGateway::log(sprintf('Error while processing order #%s : %s', $order_id, wc_print_r($e->getErrorObject(), true)), 'error'); |
| @@ -594,9 +680,9 @@ | ||
| 594 | 680 | * @param bool $status |
| 595 | 681 | * |
| 596 | 682 | * @return void |
| 597 | 683 | */ |
| 598 | - private function set_button_checkout($status) | |
| 684 | + private function set_button_checkout($status): void | |
| 599 | 685 | { |
| 600 | 686 | $this->checkout = $status; |
| 601 | 687 | } |
| 602 | 688 | |
| @@ -606,9 +692,9 @@ | ||
| 606 | 692 | * @param bool $status |
| 607 | 693 | * |
| 608 | 694 | * @return void |
| 609 | 695 | */ |
| 610 | - private function set_button_cart($status) | |
| 696 | + private function set_button_cart($status): void | |
| 611 | 697 | { |
| 612 | 698 | $this->cart = $status; |
| 613 | 699 | } |
| 614 | 700 | |
| @@ -618,9 +704,9 @@ | ||
| 618 | 704 | * @param bool $status |
| 619 | 705 | * |
| 620 | 706 | * @return void |
| 621 | 707 | */ |
| 622 | - private function set_button_product($status) | |
| 708 | + private function set_button_product($status): void | |
| 623 | 709 | { |
| 624 | 710 | $this->product = $status; |
| 625 | 711 | } |
| 626 | 712 | |
| @@ -670,9 +756,9 @@ | ||
| 670 | 756 | * @param array $carriers |
| 671 | 757 | * |
| 672 | 758 | * @return void |
| 673 | 759 | */ |
| 674 | - private function set_carriers($carriers) | |
| 760 | + private function set_carriers($carriers): void | |
| 675 | 761 | { |
| 676 | 762 | $this->carriers = $carriers; |
| 677 | 763 | } |
| 678 | 764 | } |