class-wc-payments-subscription-change-payment-method-handler.php
8.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
<?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() {
// 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'] ) ) {
return $actions;
}
$invoice_id = WC_Payments_Invoice_Service::get_order_invoice_id( $order );
$updated_pay_action = false;
// Don't show the default pay link for any WC Pay Subscription order because we don't want customer paying for them.
if ( $invoice_id ) {
$subscriptions = wcs_get_subscriptions_for_order( $order, [ 'order_type' => 'any' ] );
if ( ! empty( $subscriptions ) ) {
$subscription = array_pop( $subscriptions );
if ( $subscription && WC_Payments_Invoice_Service::get_pending_invoice_id( $subscription ) ) {
$actions['pay']['url'] = $this->get_subscription_update_payment_url( $subscription );
$updated_pay_action = true;
}
}
if ( ! $updated_pay_action ) {
unset( $actions['pay'] );
}
}
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;
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
// Check if the order is linked to a billing invoice.
$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 = wc_get_order( $order_id );
if ( $order && $order instanceof WC_Order ) {
$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 && WC_Payments_Invoice_Service::get_pending_invoice_id( $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 ( ! $subscription->has_status( 'on-hold' ) || ! WC_Payments_Subscription_Service::get_wcpay_subscription_id( $subscription ) ) {
return false;
}
$last_order = $subscription->get_last_order( 'all', 'any' );
return $last_order && $last_order->has_status( 'failed' );
}
/**
* 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(
[
'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'] ) ) { // 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;
}
}