233 lines
9.7 KiB
PHP
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;
|
|
}
|
|
}
|