Files
shuffle_and_skirmish_website/wp-content/plugins/woocommerce-payments/includes/subscriptions/class-wc-payments-subscription-change-payment-method-handler.php
2025-11-24 21:33:55 +00:00

233 lines
9.7 KiB
PHP

<?php
/**
* Class WC_Payments_Subscription_Change_Payment_Method
*
* @package WooCommerce\Payments
*/
defined( 'ABSPATH' ) || exit;
/**
* Class handling any WCPay subscription change payment method functionality.
*/
class WC_Payments_Subscription_Change_Payment_Method_Handler {
/**
* Constructor.
*/
public function __construct() {
if ( ! WC_Payments_Features::should_use_stripe_billing() ) {
return;
}
// Add an "Update card" action to all WCPay billing subscriptions with a failed renewal order.
add_filter( 'wcs_view_subscription_actions', [ $this, 'update_subscription_change_payment_button' ], 15, 2 );
add_filter( 'woocommerce_can_subscription_be_updated_to_new-payment-method', [ $this, 'can_update_payment_method' ], 15, 2 );
// Override the pay for order link on the order to redirect to a change payment method page.
add_filter( 'woocommerce_my_account_my_orders_actions', [ $this, 'update_order_pay_button' ], 15, 2 );
// Filter elements/messaging on the "Change payment method" page to reflect updating a WCPay billing card.
add_filter( 'woocommerce_subscriptions_change_payment_method_page_title', [ $this, 'change_payment_method_page_title' ], 10, 2 );
add_filter( 'woocommerce_subscriptions_change_payment_method_page_notice_message', [ $this, 'change_payment_method_page_notice' ], 10, 2 );
// Fallback to redirecting all pay for order pages for WCPay billing invoices to the update card page.
add_action( 'template_redirect', [ $this, 'redirect_pay_for_order_to_update_payment_method' ] );
add_filter( 'woocommerce_change_payment_button_text', [ $this, 'change_payment_method_form_submit_text' ] );
}
/**
* Replaces the default change payment method action for WC Pay subscriptions when the subscription needs a new payment method after a failed attempt.
*
* @param array $actions The My Account > View Subscription actions.
* @param WC_Subscription $subscription The subscription object.
*
* @return array The subscription actions.
*/
public function update_subscription_change_payment_button( $actions, $subscription ) {
if ( $this->does_subscription_need_payment_updated( $subscription ) ) {
// Override any existing button on $actions['change_payment_method'] to show "Update Card" button.
$actions['change_payment_method'] = [
'url' => $this->get_subscription_update_payment_url( $subscription ),
'name' => __( 'Update payment method', 'woocommerce-payments' ),
];
}
return $actions;
}
/**
* Updates the 'Pay' link displayed on the My Account > Orders or from a subscriptions related orders table, to make sure customers are directed to update their card.
*
* @param array $actions Order actions.
* @param WC_Order $order The WC Order object.
*
* @return array The order actions.
*/
public function update_order_pay_button( $actions, $order ) {
// If the order isn't payable, there's nothing to update.
if ( ! isset( $actions['pay'] ) || ! function_exists( 'wcs_get_subscriptions_for_order' ) ) {
return $actions;
}
// If there isn't an invoice linked to this order, there's nothing to update.
if ( ! WC_Payments_Invoice_Service::get_order_invoice_id( $order ) ) {
return $actions;
}
$subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => 'any' ] );
$subscription = ! empty( $subscriptions ) ? array_pop( $subscriptions ) : null;
// If we couldn't locate the subscription, we can assume this is a WCPay subscription by the fact the order has an invoice ID.
// As a failsafe remove the 'pay' action for this order as that's not the accepted flow for WCPay subscriptions.
if ( ! $subscription ) {
unset( $actions['pay'] );
return $actions;
}
// Only alter the pay action if the subscription needs a new payment method.
if ( $this->does_subscription_need_payment_updated( $subscription ) ) {
$actions['pay']['url'] = $this->get_subscription_update_payment_url( $subscription );
}
return $actions;
}
/**
* Filters subscription `can_be_updated_to( 'new-payment-method' )` calls to allow customers to update their subscription's payment method.
*
* @param bool $can_update Whether the subscription's payment method can be updated.
* @param WC_Subscription $subscription The WC Subscription object.
*
* @return bool Whether the subscription's payment method can be updated.
*/
public function can_update_payment_method( bool $can_update, WC_Subscription $subscription ) {
return $this->does_subscription_need_payment_updated( $subscription ) ? true : $can_update;
}
/**
* Redirects customers to update their payment method rather than pay for a WC Pay Subscription's failed order.
*/
public function redirect_pay_for_order_to_update_payment_method() {
global $wp;
// Note: There is no nonce verification for the "pay for order" action - the URL is long living.
if ( ! isset( $_GET['pay_for_order'], $_GET['key'] ) || ! empty( $_GET['change_payment_method'] ) || ( ! isset( $_GET['order_id'] ) && ! isset( $wp->query_vars['order-pay'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
if ( ! function_exists( 'wcs_get_subscriptions_for_order' ) ) {
return;
}
$order_id = ( isset( $wp->query_vars['order-pay'] ) ) ? absint( $wp->query_vars['order-pay'] ) : absint( $_GET['order_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_key = wc_clean( wp_unslash( $_GET['key'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order = wc_get_order( $order_id );
if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) || ! current_user_can( 'pay_for_order', $order->get_id() ) ) {
return;
}
// Check if the order is linked to a billing invoice.
$invoice_id = WC_Payments_Invoice_Service::get_order_invoice_id( $order );
if ( $invoice_id ) {
$subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => 'any' ] );
if ( ! empty( $subscriptions ) ) {
$subscription = array_pop( $subscriptions );
if ( $subscription && current_user_can( 'edit_shop_subscription_payment_method', $subscription->get_id() ) && $this->does_subscription_need_payment_updated( $subscription ) ) {
wp_safe_redirect( $this->get_subscription_update_payment_url( $subscription ) );
exit;
}
}
}
}
/**
* Modifies the change payment method page title (and page breadcrumbs) when updating card details for WC Pay subscriptions.
*
* @param string $title The default page title.
* @param WC_Subscription $subscription The WC Subscription object.
*
* @return string The page title.
*/
public function change_payment_method_page_title( string $title, WC_Subscription $subscription ) {
if ( $this->does_subscription_need_payment_updated( $subscription ) ) {
$title = __( 'Update payment details', 'woocommerce-payments' );
}
return $title;
}
/**
* Modifies the message shown on the change payment method page.
*
* @param string $message The default customer notice shown on the change payment method page.
* @param WC_Subscription $subscription The Subscription.
*
* @return string The customer notice shown on the change payment method page.
*/
public function change_payment_method_page_notice( string $message, WC_Subscription $subscription ) {
if ( $this->does_subscription_need_payment_updated( $subscription ) ) {
$message = __( "Your subscription's last renewal failed payment. Please update your payment details so we can reattempt payment.", 'woocommerce-payments' );
}
return $message;
}
/**
* Checks if a subscription needs to update it's WCPay payment method.
*
* @param WC_Subscription $subscription The WC Subscription object.
* @return bool Whether the subscription's last order failed and needs a new updated payment method.
*/
private function does_subscription_need_payment_updated( $subscription ) {
// We're only interested in WC Pay subscriptions that are on hold due to a failed payment.
if ( ! is_a( $subscription, 'WC_Subscription' ) || ! $subscription->has_status( 'on-hold' ) || ! WC_Payments_Subscription_Service::is_wcpay_subscription( $subscription ) ) {
return false;
}
$last_order = $subscription->get_last_order( 'all', 'any' );
return $last_order && $last_order->has_status( 'failed' ) && WC_Payments_Invoice_Service::get_pending_invoice_id( $subscription );
}
/**
* Generates the URL for the WC Pay Subscription's update payment method screen.
*
* @param WC_Subscription $subscription The WC Subscription object.
* @return string The update payment method
*/
private function get_subscription_update_payment_url( $subscription ) {
return add_query_arg( // nosemgrep: audit.php.wp.security.xss.query-arg -- no user input is used in this URL.
[
'change_payment_method' => $subscription->get_id(),
'_wpnonce' => wp_create_nonce(),
],
$subscription->get_checkout_payment_url()
);
}
/**
* Modifies the change payment method form submit button to include language about retrying payment if there's a failed order.
*
* @param string $button_text The change subscription payment method button text.
* @return string The change subscription payment method button text.
*/
public function change_payment_method_form_submit_text( $button_text ) {
if ( isset( $_GET['change_payment_method'] ) && function_exists( 'wcs_get_subscription' ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$subscription = wcs_get_subscription( wc_clean( wp_unslash( $_GET['change_payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
if ( $subscription && $this->does_subscription_need_payment_updated( $subscription ) ) {
$button_text = __( 'Update and retry payment', 'woocommerce-payments' );
}
}
return $button_text;
}
}