blob: 586997e6881fd0e8bfafb29e346eaf7ee7aad92c [file] [log] [blame]
swissChilif0cbdc32023-01-05 17:21:38 -05001<?php
2if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4}
5
6/**
7 * Trait for Subscriptions compatibility.
8 */
9trait WC_Stripe_Subscriptions_Trait {
10
11 use WC_Stripe_Subscriptions_Utilities_Trait;
12
13 /**
14 * Initialize subscription support and hooks.
15 *
16 * @since 5.6.0
17 */
18 public function maybe_init_subscriptions() {
19 if ( ! $this->is_subscriptions_enabled() ) {
20 return;
21 }
22
23 $this->supports = array_merge(
24 $this->supports,
25 [
26 'subscriptions',
27 'subscription_cancellation',
28 'subscription_suspension',
29 'subscription_reactivation',
30 'subscription_amount_changes',
31 'subscription_date_changes',
32 'subscription_payment_method_change',
33 'subscription_payment_method_change_customer',
34 'subscription_payment_method_change_admin',
35 'multiple_subscriptions',
36 ]
37 );
38
39 add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, [ $this, 'scheduled_subscription_payment' ], 10, 2 );
40 add_action( 'woocommerce_subscription_failing_payment_method_updated_' . $this->id, [ $this, 'update_failing_payment_method' ], 10, 2 );
41 add_action( 'wcs_resubscribe_order_created', [ $this, 'delete_resubscribe_meta' ], 10 );
42 add_action( 'wcs_renewal_order_created', [ $this, 'delete_renewal_meta' ], 10 );
43 add_action( 'wc_stripe_payment_fields_' . $this->id, [ $this, 'display_update_subs_payment_checkout' ] );
44 add_action( 'wc_stripe_add_payment_method_' . $this->id . '_success', [ $this, 'handle_add_payment_method_success' ], 10, 2 );
45 add_action( 'woocommerce_subscriptions_change_payment_before_submit', [ $this, 'differentiate_change_payment_method_form' ] );
46
47 // Display the payment method used for a subscription in the "My Subscriptions" table.
48 add_filter( 'woocommerce_my_subscriptions_payment_method', [ $this, 'maybe_render_subscription_payment_method' ], 10, 2 );
49
50 // Allow store managers to manually set Stripe as the payment method on a subscription.
51 add_filter( 'woocommerce_subscription_payment_meta', [ $this, 'add_subscription_payment_meta' ], 10, 2 );
52 add_filter( 'woocommerce_subscription_validate_payment_meta', [ $this, 'validate_subscription_payment_meta' ], 10, 2 );
53 add_filter( 'wc_stripe_display_save_payment_method_checkbox', [ $this, 'display_save_payment_method_checkbox' ] );
54
55 /*
56 * WC subscriptions hooks into the "template_redirect" hook with priority 100.
57 * If the screen is "Pay for order" and the order is a subscription renewal, it redirects to the plain checkout.
58 * See: https://github.com/woocommerce/woocommerce-subscriptions/blob/99a75687e109b64cbc07af6e5518458a6305f366/includes/class-wcs-cart-renewal.php#L165
59 * If we are in the "You just need to authorize SCA" flow, we don't want that redirection to happen.
60 */
61 add_action( 'template_redirect', [ $this, 'remove_order_pay_var' ], 99 );
62 add_action( 'template_redirect', [ $this, 'restore_order_pay_var' ], 101 );
63 }
64
65 /**
66 * Displays a checkbox to allow users to update all subs payments with new
67 * payment.
68 *
69 * @since 4.1.11
70 */
71 public function display_update_subs_payment_checkout() {
72 $subs_statuses = apply_filters( 'wc_stripe_update_subs_payment_method_card_statuses', [ 'active' ] );
73 if (
74 apply_filters( 'wc_stripe_display_update_subs_payment_method_card_checkbox', true ) &&
75 wcs_user_has_subscription( get_current_user_id(), '', $subs_statuses ) &&
76 is_add_payment_method_page()
77 ) {
78 $label = esc_html( apply_filters( 'wc_stripe_save_to_subs_text', __( 'Update the Payment Method used for all of my active subscriptions.', 'woocommerce-gateway-stripe' ) ) );
79 $id = sprintf( 'wc-%1$s-update-subs-payment-method-card', $this->id );
80 woocommerce_form_field(
81 $id,
82 [
83 'type' => 'checkbox',
84 'label' => $label,
85 'default' => apply_filters( 'wc_stripe_save_to_subs_checked', false ),
86 ]
87 );
88 }
89 }
90
91 /**
92 * Updates all active subscriptions payment method.
93 *
94 * @since 4.1.11
95 *
96 * @param string $source_id
97 * @param object $source_object
98 */
99 public function handle_add_payment_method_success( $source_id, $source_object ) {
100 if ( isset( $_POST[ 'wc-' . $this->id . '-update-subs-payment-method-card' ] ) ) {
101 $all_subs = wcs_get_users_subscriptions();
102 $subs_statuses = apply_filters( 'wc_stripe_update_subs_payment_method_card_statuses', [ 'active' ] );
103 $stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
104
105 if ( ! empty( $all_subs ) ) {
106 foreach ( $all_subs as $sub ) {
107 if ( $sub->has_status( $subs_statuses ) ) {
108 WC_Subscriptions_Change_Payment_Gateway::update_payment_method(
109 $sub,
110 $this->id,
111 [
112 'post_meta' => [
113 '_stripe_source_id' => [ 'value' => $source_id ],
114 '_stripe_customer_id' => [ 'value' => $stripe_customer->get_id() ],
115 ],
116 ]
117 );
118 }
119 }
120 }
121 }
122 }
123
124 /**
125 * Render a dummy element in the "Change payment method" form (that does not appear in the "Pay for order" form)
126 * which can be checked to determine proper SCA handling to apply for each form.
127 *
128 * @since 4.6.1
129 */
130 public function differentiate_change_payment_method_form() {
131 echo '<input type="hidden" id="wc-stripe-change-payment-method" />';
132 }
133
134 /**
135 * Maybe process payment method change for subscriptions.
136 *
137 * @since 5.6.0
138 *
139 * @param int $order_id
140 * @return bool
141 */
142 public function maybe_change_subscription_payment_method( $order_id ) {
143 return (
144 $this->is_subscriptions_enabled() &&
145 $this->has_subscription( $order_id ) &&
146 $this->is_changing_payment_method_for_subscription()
147 );
148 }
149
150 /**
151 * Process the payment method change for subscriptions.
152 *
153 * @since 5.6.0
154 *
155 * @param int $order_id
156 * @return array|null
157 */
158 public function process_change_subscription_payment_method( $order_id ) {
159 try {
160 $subscription = wc_get_order( $order_id );
161 $prepared_source = $this->prepare_source( get_current_user_id(), true );
162
163 $this->maybe_disallow_prepaid_card( $prepared_source->source_object );
164 $this->check_source( $prepared_source );
165 $this->save_source_to_order( $subscription, $prepared_source );
166
167 do_action( 'wc_stripe_change_subs_payment_method_success', $prepared_source->source, $prepared_source );
168
169 return [
170 'result' => 'success',
171 'redirect' => $this->get_return_url( $subscription ),
172 ];
173 } catch ( WC_Stripe_Exception $e ) {
174 wc_add_notice( $e->getLocalizedMessage(), 'error' );
175 WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
176 }
177 }
178
179 /**
180 * Scheduled_subscription_payment function.
181 *
182 * @param $amount_to_charge float The amount to charge.
183 * @param $renewal_order WC_Order A WC_Order object created to record the renewal payment.
184 */
185 public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) {
186 $this->process_subscription_payment( $amount_to_charge, $renewal_order, true, false );
187 }
188
189 /**
190 * Process_subscription_payment function.
191 *
192 * @since 3.0
193 * @since 4.0.4 Add third parameter flag to retry.
194 * @since 4.1.0 Add fourth parameter to log previous errors.
195 * @since 5.6.0 Process renewal payments for SEPA and UPE.
196 *
197 * @param float $amount
198 * @param mixed $renewal_order
199 * @param bool $retry Should we retry the process?
200 * @param object $previous_error
201 */
202 public function process_subscription_payment( $amount, $renewal_order, $retry = true, $previous_error = false ) {
203 try {
204 $order_id = $renewal_order->get_id();
205
206 // Unlike regular off-session subscription payments, early renewals are treated as on-session payments, involving the customer.
207 // This makes the SCA authorization popup show up for the "Renew early" modal (Subscriptions settings > Accept Early Renewal Payments via a Modal).
208 // Note: Currently available for non-UPE credit card only.
209 if ( isset( $_REQUEST['process_early_renewal'] ) && 'stripe' === $this->id && ! WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) { // phpcs:ignore WordPress.Security.NonceVerification
210 $response = $this->process_payment( $order_id, true, false, $previous_error, true );
211
212 if ( 'success' === $response['result'] && isset( $response['payment_intent_secret'] ) ) {
213 $verification_url = add_query_arg(
214 [
215 'order' => $order_id,
216 'nonce' => wp_create_nonce( 'wc_stripe_confirm_pi' ),
217 'redirect_to' => esc_url_raw( remove_query_arg( [ 'process_early_renewal', 'subscription_id', 'wcs_nonce' ] ) ),
218 'early_renewal' => true,
219 ],
220 WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
221 );
222
223 echo wp_json_encode(
224 [
225 'stripe_sca_required' => true,
226 'intent_secret' => $response['payment_intent_secret'],
227 'redirect_url' => $verification_url,
228 ]
229 );
230
231 exit;
232 }
233
234 // Hijack all other redirects in order to do the redirection in JavaScript.
235 add_action( 'wp_redirect', [ $this, 'redirect_after_early_renewal' ], 100 );
236
237 return;
238 }
239
240 // Check for an existing intent, which is associated with the order.
241 if ( $this->has_authentication_already_failed( $renewal_order ) ) {
242 return;
243 }
244
245 // Get source from order
246 $prepared_source = $this->prepare_order_source( $renewal_order );
247 $source_object = $prepared_source->source_object;
248
249 if ( ! $prepared_source->customer ) {
250 throw new WC_Stripe_Exception(
251 'Failed to process renewal for order ' . $renewal_order->get_id() . '. Stripe customer id is missing in the order',
252 __( 'Customer not found', 'woocommerce-gateway-stripe' )
253 );
254 }
255
256 WC_Stripe_Logger::log( "Info: Begin processing subscription payment for order {$order_id} for the amount of {$amount}" );
257
258 /*
259 * If we're doing a retry and source is chargeable, we need to pass
260 * a different idempotency key and retry for success.
261 */
262 if ( is_object( $source_object ) && empty( $source_object->error ) && $this->need_update_idempotency_key( $source_object, $previous_error ) ) {
263 add_filter( 'wc_stripe_idempotency_key', [ $this, 'change_idempotency_key' ], 10, 2 );
264 }
265
266 if ( ( $this->is_no_such_source_error( $previous_error ) || $this->is_no_linked_source_error( $previous_error ) ) && apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
267 // Passing empty source will charge customer default.
268 $prepared_source->source = '';
269 }
270
271 // If the payment gateway is SEPA, use the charges API.
272 // TODO: Remove when SEPA is migrated to payment intents.
273 if ( 'stripe_sepa' === $this->id ) {
274 $request = $this->generate_payment_request( $renewal_order, $prepared_source );
275 $request['capture'] = 'true';
276 $request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $request['currency'] );
277 $response = WC_Stripe_API::request( $request );
278
279 $is_authentication_required = false;
280 } else {
281 $this->lock_order_payment( $renewal_order );
282 $response = $this->create_and_confirm_intent_for_off_session( $renewal_order, $prepared_source, $amount );
283 $is_authentication_required = $this->is_authentication_required_for_payment( $response );
284 }
285
286 // It's only a failed payment if it's an error and it's not of the type 'authentication_required'.
287 // If it's 'authentication_required', then we should email the user and ask them to authenticate.
288 if ( ! empty( $response->error ) && ! $is_authentication_required ) {
289 // We want to retry.
290 if ( $this->is_retryable_error( $response->error ) ) {
291 if ( $retry ) {
292 // Don't do anymore retries after this.
293 if ( 5 <= $this->retry_interval ) {
294 return $this->process_subscription_payment( $amount, $renewal_order, false, $response->error );
295 }
296
297 sleep( $this->retry_interval );
298
299 $this->retry_interval++;
300
301 return $this->process_subscription_payment( $amount, $renewal_order, true, $response->error );
302 } else {
303 $localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
304 $renewal_order->add_order_note( $localized_message );
305 throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
306 }
307 }
308
309 $localized_messages = WC_Stripe_Helper::get_localized_messages();
310
311 if ( 'card_error' === $response->error->type ) {
312 $localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
313 } else {
314 $localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
315 }
316
317 $renewal_order->add_order_note( $localized_message );
318
319 throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
320 }
321
322 // Either the charge was successfully captured, or it requires further authentication.
323 if ( $is_authentication_required ) {
324 do_action( 'wc_gateway_stripe_process_payment_authentication_required', $renewal_order, $response );
325
326 $error_message = __( 'This transaction requires authentication.', 'woocommerce-gateway-stripe' );
327 $renewal_order->add_order_note( $error_message );
328
329 $charge = end( $response->error->payment_intent->charges->data );
330 $id = $charge->id;
331 $order_id = $renewal_order->get_id();
332
333 $renewal_order->set_transaction_id( $id );
334 /* translators: %s is the charge Id */
335 $renewal_order->update_status( 'failed', sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $id ) );
336 if ( is_callable( [ $renewal_order, 'save' ] ) ) {
337 $renewal_order->save();
338 }
339 } else {
340 // The charge was successfully captured
341 do_action( 'wc_gateway_stripe_process_payment', $response, $renewal_order );
342
343 // Use the last charge within the intent or the full response body in case of SEPA.
344 $this->process_response( isset( $response->charges ) ? end( $response->charges->data ) : $response, $renewal_order );
345 }
346
347 // TODO: Remove when SEPA is migrated to payment intents.
348 if ( 'stripe_sepa' !== $this->id ) {
349 $this->unlock_order_payment( $renewal_order );
350 }
351 } catch ( WC_Stripe_Exception $e ) {
352 WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
353
354 do_action( 'wc_gateway_stripe_process_payment_error', $e, $renewal_order );
355
356 /* translators: error message */
357 $renewal_order->update_status( 'failed' );
358 }
359 }
360
361 /**
362 * Updates other subscription sources.
363 *
364 * @since 5.6.0
365 */
366 public function maybe_update_source_on_subscription_order( $order, $source ) {
367 if ( ! $this->is_subscriptions_enabled() ) {
368 return;
369 }
370
371 $order_id = $order->get_id();
372
373 // Also store it on the subscriptions being purchased or paid for in the order
374 if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) {
375 $subscriptions = wcs_get_subscriptions_for_order( $order_id );
376 } elseif ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order_id ) ) {
377 $subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
378 } else {
379 $subscriptions = [];
380 }
381
382 foreach ( $subscriptions as $subscription ) {
383 $subscription_id = $subscription->get_id();
384 update_post_meta( $subscription_id, '_stripe_customer_id', $source->customer );
385
386 if ( ! empty( $source->payment_method ) ) {
387 update_post_meta( $subscription_id, '_stripe_source_id', $source->payment_method );
388 } else {
389 update_post_meta( $subscription_id, '_stripe_source_id', $source->source );
390 }
391 }
392 }
393
394 /**
395 * Don't transfer Stripe customer/token meta to resubscribe orders.
396 *
397 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
398 */
399 public function delete_resubscribe_meta( $resubscribe_order ) {
400 delete_post_meta( $resubscribe_order->get_id(), '_stripe_customer_id' );
401 delete_post_meta( $resubscribe_order->get_id(), '_stripe_source_id' );
402 // For BW compat will remove in future.
403 delete_post_meta( $resubscribe_order->get_id(), '_stripe_card_id' );
404 // Delete payment intent ID.
405 delete_post_meta( $resubscribe_order->get_id(), '_stripe_intent_id' );
406 $this->delete_renewal_meta( $resubscribe_order );
407 }
408
409 /**
410 * Don't transfer Stripe fee/ID meta to renewal orders.
411 *
412 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
413 */
414 public function delete_renewal_meta( $renewal_order ) {
415 WC_Stripe_Helper::delete_stripe_fee( $renewal_order );
416 WC_Stripe_Helper::delete_stripe_net( $renewal_order );
417
418 // Delete payment intent ID.
419 delete_post_meta( $renewal_order->get_id(), '_stripe_intent_id' );
420
421 return $renewal_order;
422 }
423
424 /**
425 * Update the customer_id for a subscription after using Stripe to complete a payment to make up for
426 * an automatic renewal payment which previously failed.
427 *
428 * @param WC_Subscription $subscription The subscription for which the failing payment method relates.
429 * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment).
430 * @return void
431 */
432 public function update_failing_payment_method( $subscription, $renewal_order ) {
433 update_post_meta( $subscription->get_id(), '_stripe_customer_id', $renewal_order->get_meta( '_stripe_customer_id', true ) );
434 update_post_meta( $subscription->get_id(), '_stripe_source_id', $renewal_order->get_meta( '_stripe_source_id', true ) );
435 }
436
437 /**
438 * Include the payment meta data required to process automatic recurring payments so that store managers can
439 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
440 *
441 * @since 2.5
442 *
443 * @param array $payment_meta associative array of meta data required for automatic payments
444 * @param WC_Subscription $subscription An instance of a subscription object
445 * @return array
446 */
447 public function add_subscription_payment_meta( $payment_meta, $subscription ) {
448 $subscription_id = $subscription->get_id();
449 $source_id = get_post_meta( $subscription_id, '_stripe_source_id', true );
450
451 // For BW compat will remove in future.
452 if ( empty( $source_id ) ) {
453 $source_id = get_post_meta( $subscription_id, '_stripe_card_id', true );
454
455 // Take this opportunity to update the key name.
456 update_post_meta( $subscription_id, '_stripe_source_id', $source_id );
457 delete_post_meta( $subscription_id, '_stripe_card_id', $source_id );
458 }
459
460 $payment_meta[ $this->id ] = [
461 'post_meta' => [
462 '_stripe_customer_id' => [
463 'value' => get_post_meta( $subscription_id, '_stripe_customer_id', true ),
464 'label' => 'Stripe Customer ID',
465 ],
466 '_stripe_source_id' => [
467 'value' => $source_id,
468 'label' => 'Stripe Source ID',
469 ],
470 ],
471 ];
472
473 return $payment_meta;
474 }
475
476 /**
477 * Validate the payment meta data required to process automatic recurring payments so that store managers can
478 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
479 *
480 * @since 2.5
481 * @since 4.0.4 Stripe sourd id field no longer needs to be required.
482 *
483 * @param string $payment_method_id The ID of the payment method to validate
484 * @param array $payment_meta associative array of meta data required for automatic payments
485 * @return array
486 */
487 public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ) {
488 if ( $this->id === $payment_method_id ) {
489
490 if ( ! isset( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) || empty( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) ) {
491
492 // Allow empty stripe customer id during subscription renewal. It will be added when processing payment if required.
493 if ( ! isset( $_POST['wc_order_action'] ) || 'wcs_process_renewal' !== $_POST['wc_order_action'] ) {
494 throw new Exception( __( 'A "Stripe Customer ID" value is required.', 'woocommerce-gateway-stripe' ) );
495 }
496 } elseif ( 0 !== strpos( $payment_meta['post_meta']['_stripe_customer_id']['value'], 'cus_' ) ) {
497 throw new Exception( __( 'Invalid customer ID. A valid "Stripe Customer ID" must begin with "cus_".', 'woocommerce-gateway-stripe' ) );
498 }
499
500 if (
501 ! empty( $payment_meta['post_meta']['_stripe_source_id']['value'] ) && (
502 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'card_' )
503 && 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'src_' )
504 && 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'pm_' )
505 )
506 ) {
507 throw new Exception( __( 'Invalid source ID. A valid source "Stripe Source ID" must begin with "src_", "pm_", or "card_".', 'woocommerce-gateway-stripe' ) );
508 }
509 }
510 }
511
512 /**
513 * Render the payment method used for a subscription in the "My Subscriptions" table
514 *
515 * @since 1.7.5
516 * @version 5.6.0
517 *
518 * @param string $payment_method_to_display the default payment method text to display
519 * @param WC_Subscription $subscription the subscription details
520 * @return string the subscription payment method
521 */
522 public function maybe_render_subscription_payment_method( $payment_method_to_display, $subscription ) {
523 $customer_user = $subscription->get_customer_id();
524
525 // bail for other payment methods
526 if ( $subscription->get_payment_method() !== $this->id || ! $customer_user ) {
527 return $payment_method_to_display;
528 }
529
530 $stripe_source_id = get_post_meta( $subscription->get_id(), '_stripe_source_id', true );
531
532 // For BW compat will remove in future.
533 if ( empty( $stripe_source_id ) ) {
534 $stripe_source_id = get_post_meta( $subscription->get_id(), '_stripe_card_id', true );
535
536 // Take this opportunity to update the key name.
537 update_post_meta( $subscription->get_id(), '_stripe_source_id', $stripe_source_id );
538 }
539
540 $stripe_customer = new WC_Stripe_Customer();
541 $stripe_customer_id = get_post_meta( $subscription->get_id(), '_stripe_customer_id', true );
542
543 // If we couldn't find a Stripe customer linked to the subscription, fallback to the user meta data.
544 if ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) {
545 $user_id = $customer_user;
546 $stripe_customer_id = get_user_option( '_stripe_customer_id', $user_id );
547 $stripe_source_id = get_user_option( '_stripe_source_id', $user_id );
548
549 // For BW compat will remove in future.
550 if ( empty( $stripe_source_id ) ) {
551 $stripe_source_id = get_user_option( '_stripe_card_id', $user_id );
552
553 // Take this opportunity to update the key name.
554 update_user_option( $user_id, '_stripe_source_id', $stripe_source_id, false );
555 }
556 }
557
558 // If we couldn't find a Stripe customer linked to the account, fallback to the order meta data.
559 if ( ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) && false !== $subscription->get_parent() ) {
560 $stripe_customer_id = get_post_meta( $subscription->get_parent_id(), '_stripe_customer_id', true );
561 $stripe_source_id = get_post_meta( $subscription->get_parent_id(), '_stripe_source_id', true );
562
563 // For BW compat will remove in future.
564 if ( empty( $stripe_source_id ) ) {
565 $stripe_source_id = get_post_meta( $subscription->get_parent_id(), '_stripe_card_id', true );
566
567 // Take this opportunity to update the key name.
568 update_post_meta( $subscription->get_parent_id(), '_stripe_source_id', $stripe_source_id );
569 }
570 }
571
572 $stripe_customer->set_id( $stripe_customer_id );
573
574 // Retrieve all possible payment methods for subscriptions.
575 $sources = array_merge(
576 $stripe_customer->get_payment_methods( 'card' ),
577 $stripe_customer->get_payment_methods( 'sepa_debit' )
578 );
579 $payment_method_to_display = __( 'N/A', 'woocommerce-gateway-stripe' );
580
581 if ( $sources ) {
582 foreach ( $sources as $source ) {
583 if ( $source->id === $stripe_source_id ) {
584 $card = false;
585 if ( isset( $source->type ) && 'card' === $source->type ) {
586 $card = $source->card;
587 } elseif ( isset( $source->object ) && 'card' === $source->object ) {
588 $card = $source;
589 }
590
591 if ( $card ) {
592 /* translators: 1) card brand 2) last 4 digits */
593 $payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $card->brand ) ? $card->brand : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $card->last4 );
594 } elseif ( $source->sepa_debit ) {
595 /* translators: 1) last 4 digits of SEPA Direct Debit */
596 $payment_method_to_display = sprintf( __( 'Via SEPA Direct Debit ending in %1$s', 'woocommerce-gateway-stripe' ), $source->sepa_debit->last4 );
597 }
598
599 break;
600 }
601 }
602 }
603
604 return $payment_method_to_display;
605 }
606
607 /**
608 * If this is the "Pass the SCA challenge" flow, remove a variable that is checked by WC Subscriptions
609 * so WC Subscriptions doesn't redirect to the checkout
610 */
611 public function remove_order_pay_var() {
612 global $wp;
613 if ( isset( $_GET['wc-stripe-confirmation'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
614 $this->order_pay_var = $wp->query_vars['order-pay'];
615 $wp->query_vars['order-pay'] = null;
616 }
617 }
618
619 /**
620 * Restore the variable that was removed in remove_order_pay_var()
621 */
622 public function restore_order_pay_var() {
623 global $wp;
624 if ( isset( $this->order_pay_var ) ) {
625 $wp->query_vars['order-pay'] = $this->order_pay_var;
626 }
627 }
628
629 /**
630 * Checks if a renewal already failed because a manual authentication is required.
631 *
632 * @param WC_Order $renewal_order The renewal order.
633 * @return boolean
634 */
635 public function has_authentication_already_failed( $renewal_order ) {
636 $existing_intent = $this->get_intent_from_order( $renewal_order );
637
638 if (
639 ! $existing_intent
640 || 'requires_payment_method' !== $existing_intent->status
641 || empty( $existing_intent->last_payment_error )
642 || 'authentication_required' !== $existing_intent->last_payment_error->code
643 ) {
644 return false;
645 }
646
647 // Make sure all emails are instantiated.
648 WC_Emails::instance();
649
650 /**
651 * A payment attempt failed because SCA authentication is required.
652 *
653 * @param WC_Order $renewal_order The order that is being renewed.
654 */
655 do_action( 'wc_gateway_stripe_process_payment_authentication_required', $renewal_order );
656
657 // Fail the payment attempt (order would be currently pending because of retry rules).
658 $charge = end( $existing_intent->charges->data );
659 $charge_id = $charge->id;
660 /* translators: %s is the stripe charge Id */
661 $renewal_order->update_status( 'failed', sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $charge_id ) );
662
663 return true;
664 }
665
666 /**
667 * Hijacks `wp_redirect` in order to generate a JS-friendly object with the URL.
668 *
669 * @param string $url The URL that Subscriptions attempts a redirect to.
670 * @return void
671 */
672 public function redirect_after_early_renewal( $url ) {
673 echo wp_json_encode(
674 [
675 'stripe_sca_required' => false,
676 'redirect_url' => $url,
677 ]
678 );
679
680 exit;
681 }
682
683 /**
684 * Once an intent has been verified, perform some final actions for early renewals.
685 *
686 * @param WC_Order $order The renewal order.
687 * @param stdClass $intent The Payment Intent object.
688 */
689 protected function maybe_process_subscription_early_renewal_success( $order, $intent ) {
690 if ( $this->is_subscriptions_enabled() && isset( $_GET['early_renewal'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
691 wcs_update_dates_after_early_renewal( wcs_get_subscription( $order->get_meta( '_subscription_renewal' ) ), $order );
692 wc_add_notice( __( 'Your early renewal order was successful.', 'woocommerce-gateway-stripe' ), 'success' );
693 }
694 }
695
696 /**
697 * During early renewals, instead of failing the renewal order, delete it and let Subs redirect to the checkout.
698 *
699 * @param WC_Order $order The renewal order.
700 * @param stdClass $intent The Payment Intent object (unused).
701 */
702 protected function maybe_process_subscription_early_renewal_failure( $order, $intent ) {
703 if ( $this->is_subscriptions_enabled() && isset( $_GET['early_renewal'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
704 $order->delete( true );
705 wc_add_notice( __( 'Payment authorization for the renewal order was unsuccessful, please try again.', 'woocommerce-gateway-stripe' ), 'error' );
706 $renewal_url = wcs_get_early_renewal_url( wcs_get_subscription( $order->get_meta( '_subscription_renewal' ) ) );
707 wp_safe_redirect( $renewal_url );
708 exit;
709 }
710 }
711}