blob: d18858930226ceca118b137de07498803cc3d18b [file] [log] [blame]
swissChilif0cbdc32023-01-05 17:21:38 -05001<?php
2
3if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5}
6
7/**
8* Class that handles UPE payment method.
9*
10* @extends WC_Gateway_Stripe
11*
12* @since 5.5.0
13*/
14class WC_Stripe_UPE_Payment_Gateway extends WC_Gateway_Stripe {
15
16 const ID = 'stripe';
17
18 /**
19 * Upe Available Methods
20 *
21 * @type WC_Stripe_UPE_Payment_Method[]
22 */
23 const UPE_AVAILABLE_METHODS = [
24 WC_Stripe_UPE_Payment_Method_CC::class,
25 WC_Stripe_UPE_Payment_Method_Giropay::class,
26 WC_Stripe_UPE_Payment_Method_Eps::class,
27 WC_Stripe_UPE_Payment_Method_Bancontact::class,
28 WC_Stripe_UPE_Payment_Method_Boleto::class,
29 WC_Stripe_UPE_Payment_Method_Ideal::class,
30 WC_Stripe_UPE_Payment_Method_Oxxo::class,
31 WC_Stripe_UPE_Payment_Method_Sepa::class,
32 WC_Stripe_UPE_Payment_Method_P24::class,
33 WC_Stripe_UPE_Payment_Method_Sofort::class,
34 WC_Stripe_UPE_Payment_Method_Link::class,
35 ];
36
37 const UPE_APPEARANCE_TRANSIENT = 'wc_stripe_upe_appearance';
38
39 const WC_BLOCKS_UPE_APPEARANCE_TRANSIENT = 'wc_stripe_wc_blocks_upe_appearance';
40
41 /**
42 * Stripe intents that are treated as successfully created.
43 *
44 * @type array
45 */
46 const SUCCESSFUL_INTENT_STATUS = [ 'succeeded', 'requires_capture', 'processing' ];
47
48 /**
49 * Notices (array)
50 *
51 * @var array
52 */
53 public $notices = [];
54
55 /**
56 * Is test mode active?
57 *
58 * @var bool
59 */
60 public $testmode;
61
62 /**
63 * Alternate credit card statement name
64 *
65 * @var bool
66 */
67 public $statement_descriptor;
68
69 /**
70 * Are saved cards enabled
71 *
72 * @var bool
73 */
74 public $saved_cards;
75
76 /**
77 * API access secret key
78 *
79 * @var string
80 */
81 public $secret_key;
82
83 /**
84 * Api access publishable key
85 *
86 * @var string
87 */
88 public $publishable_key;
89
90 /**
91 * Array mapping payment method string IDs to classes
92 *
93 * @var WC_Stripe_UPE_Payment_Method[]
94 */
95 public $payment_methods = [];
96
97 /**
98 * Constructor
99 */
100 public function __construct() {
101 $this->id = self::ID;
102 $this->method_title = __( 'Stripe', 'woocommerce-gateway-stripe' );
103 /* translators: link */
104 $this->method_description = __( 'Accept debit and credit cards in 135+ currencies, methods such as SEPA, and one-touch checkout with Apple Pay.', 'woocommerce-gateway-stripe' );
105 $this->has_fields = true;
106 $this->supports = [
107 'products',
108 'refunds',
109 'tokenization',
110 'add_payment_method',
111 ];
112
113 $this->payment_methods = [];
114 foreach ( self::UPE_AVAILABLE_METHODS as $payment_method_class ) {
115 $payment_method = new $payment_method_class();
116 $this->payment_methods[ $payment_method->get_id() ] = $payment_method;
117 }
118
119 // Load the form fields.
120 $this->init_form_fields();
121
122 // Load the settings.
123 $this->init_settings();
124
125 // Check if subscriptions are enabled and add support for them.
126 $this->maybe_init_subscriptions();
127
128 // Check if pre-orders are enabled and add support for them.
129 $this->maybe_init_pre_orders();
130
131 $main_settings = get_option( 'woocommerce_stripe_settings' );
132 $this->title = ! empty( $this->get_option( 'title_upe' ) ) ? $this->get_option( 'title_upe' ) : $this->form_fields['title_upe']['default'];
133 $this->description = '';
134 $this->enabled = $this->get_option( 'enabled' );
135 $this->saved_cards = 'yes' === $this->get_option( 'saved_cards' );
136 $this->testmode = ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode'];
137 $this->publishable_key = ! empty( $main_settings['publishable_key'] ) ? $main_settings['publishable_key'] : '';
138 $this->secret_key = ! empty( $main_settings['secret_key'] ) ? $main_settings['secret_key'] : '';
139 $this->statement_descriptor = ! empty( $main_settings['statement_descriptor'] ) ? $main_settings['statement_descriptor'] : '';
140
141 $enabled_at_checkout_payment_methods = $this->get_upe_enabled_at_checkout_payment_method_ids();
142 if ( count( $enabled_at_checkout_payment_methods ) === 1 ) {
143 $this->title = $this->payment_methods[ $enabled_at_checkout_payment_methods[0] ]->get_title();
144 }
145
146 // When feature flags are enabled, title shows the count of enabled payment methods in settings page only.
147 if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() && WC_Stripe_Feature_Flags::is_upe_preview_enabled() && isset( $_GET['page'] ) && 'wc-settings' === $_GET['page'] ) {
148 $enabled_payment_methods_count = count( $this->get_upe_enabled_payment_method_ids() );
149 $this->title = $enabled_payment_methods_count ?
150 /* translators: $1. Count of enabled payment methods. */
151 sprintf( _n( '%d payment method', '%d payment methods', $enabled_payment_methods_count, 'woocommerce-gateway-stripe' ), $enabled_payment_methods_count )
152 : $this->method_title;
153 }
154
155 if ( $this->testmode ) {
156 $this->publishable_key = ! empty( $main_settings['test_publishable_key'] ) ? $main_settings['test_publishable_key'] : '';
157 $this->secret_key = ! empty( $main_settings['test_secret_key'] ) ? $main_settings['test_secret_key'] : '';
158 }
159
160 add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
161 add_action( 'wp_enqueue_scripts', [ $this, 'payment_scripts' ] );
162
163 // Needed for 3DS compatibility when checking out with PRBs..
164 // Copied from WC_Gateway_Stripe::__construct().
165 add_filter( 'woocommerce_payment_successful_result', [ $this, 'modify_successful_payment_result' ], 99999, 2 );
166 }
167
168 /**
169 * Hides refund through stripe when payment method does not allow refund
170 *
171 * @param WC_Order $order
172 *
173 * @return array|bool
174 */
175 public function can_refund_order( $order ) {
176 $upe_payment_type = $order->get_meta( '_stripe_upe_payment_type' );
177
178 if ( ! $upe_payment_type ) {
179 return true;
180 }
181
182 return $this->payment_methods[ $upe_payment_type ]->can_refund_via_stripe();
183 }
184
185 /**
186 * Return the gateway icon - None for UPE.
187 */
188 public function get_icon() {
189 return apply_filters( 'woocommerce_gateway_icon', null, $this->id );
190 }
191
192 /**
193 * Initialize Gateway Settings Form Fields.
194 */
195 public function init_form_fields() {
196 $this->form_fields = require WC_STRIPE_PLUGIN_PATH . '/includes/admin/stripe-settings.php';
197 unset( $this->form_fields['inline_cc_form'] );
198 unset( $this->form_fields['title'] );
199 unset( $this->form_fields['description'] );
200 }
201
202 /**
203 * Outputs scripts used for stripe payment
204 */
205 public function payment_scripts() {
206 if (
207 ! is_product()
208 && ! WC_Stripe_Helper::has_cart_or_checkout_on_current_page()
209 && ! isset( $_GET['pay_for_order'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
210 && ! is_add_payment_method_page() ) {
211 return;
212 }
213
214 if ( is_product() && ! WC_Stripe_Helper::should_load_scripts_on_product_page() ) {
215 return;
216 }
217
218 if ( is_cart() && ! WC_Stripe_Helper::should_load_scripts_on_cart_page() ) {
219 return;
220 }
221
222 $asset_path = WC_STRIPE_PLUGIN_PATH . '/build/checkout_upe.asset.php';
223 $version = WC_STRIPE_VERSION;
224 $dependencies = [];
225 if ( file_exists( $asset_path ) ) {
226 $asset = require $asset_path;
227 $version = is_array( $asset ) && isset( $asset['version'] )
228 ? $asset['version']
229 : $version;
230 $dependencies = is_array( $asset ) && isset( $asset['dependencies'] )
231 ? $asset['dependencies']
232 : $dependencies;
233 }
234
235 wp_register_script(
236 'stripe',
237 'https://js.stripe.com/v3/',
238 [],
239 '3.0',
240 true
241 );
242
243 wp_register_script(
244 'wc-stripe-upe-classic',
245 WC_STRIPE_PLUGIN_URL . '/build/upe_classic.js',
246 array_merge( [ 'stripe', 'wc-checkout' ], $dependencies ),
247 $version,
248 true
249 );
250 wp_set_script_translations(
251 'wc-stripe-upe-classic',
252 'woocommerce-gateway-stripe'
253 );
254
255 wp_localize_script(
256 'wc-stripe-upe-classic',
257 'wc_stripe_upe_params',
258 apply_filters( 'wc_stripe_upe_params', $this->javascript_params() )
259 );
260
261 wp_register_style(
262 'wc-stripe-upe-classic',
263 WC_STRIPE_PLUGIN_URL . '/build/upe_classic.css',
264 [],
265 $version
266 );
267
268 wp_enqueue_script( 'wc-stripe-upe-classic' );
269 wp_enqueue_style( 'wc-stripe-upe-classic' );
270
271 wp_register_style( 'stripelink_styles', plugins_url( 'assets/css/stripe-link.css', WC_STRIPE_MAIN_FILE ), [], WC_STRIPE_VERSION );
272 wp_enqueue_style( 'stripelink_styles' );
273 }
274
275 /**
276 * Returns the JavaScript configuration object used on the product, cart, and checkout pages.
277 *
278 * @return array The configuration object to be loaded to JS.
279 */
280 public function javascript_params() {
281 global $wp;
282
283 $stripe_params = [
284 'title' => $this->title,
285 'isUPEEnabled' => true,
286 'key' => $this->publishable_key,
287 'locale' => WC_Stripe_Helper::convert_wc_locale_to_stripe_locale( get_locale() ),
288 ];
289
290 $enabled_billing_fields = [];
291 foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) {
292 if ( ! isset( $billing_field_options['enabled'] ) || $billing_field_options['enabled'] ) {
293 $enabled_billing_fields[] = $billing_field;
294 }
295 }
296
297 $stripe_params['isCheckout'] = is_checkout() && empty( $_GET['pay_for_order'] ); // wpcs: csrf ok.
298 $stripe_params['return_url'] = $this->get_stripe_return_url();
299 $stripe_params['ajax_url'] = WC_AJAX::get_endpoint( '%%endpoint%%' );
300 $stripe_params['createPaymentIntentNonce'] = wp_create_nonce( 'wc_stripe_create_payment_intent_nonce' );
301 $stripe_params['updatePaymentIntentNonce'] = wp_create_nonce( 'wc_stripe_update_payment_intent_nonce' );
302 $stripe_params['createSetupIntentNonce'] = wp_create_nonce( 'wc_stripe_create_setup_intent_nonce' );
303 $stripe_params['updateFailedOrderNonce'] = wp_create_nonce( 'wc_stripe_update_failed_order_nonce' );
304 $stripe_params['upeAppearance'] = get_transient( self::UPE_APPEARANCE_TRANSIENT );
305 $stripe_params['wcBlocksUPEAppearance'] = get_transient( self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT );
306 $stripe_params['saveUPEAppearanceNonce'] = wp_create_nonce( 'wc_stripe_save_upe_appearance_nonce' );
307 $stripe_params['paymentMethodsConfig'] = $this->get_enabled_payment_method_config();
308 $stripe_params['genericErrorMessage'] = __( 'There was a problem processing the payment. Please check your email inbox and refresh the page to try again.', 'woocommerce-gateway-stripe' );
309 $stripe_params['accountDescriptor'] = $this->statement_descriptor;
310 $stripe_params['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' );
311 $stripe_params['enabledBillingFields'] = $enabled_billing_fields;
312
313 if ( is_wc_endpoint_url( 'order-pay' ) ) {
314 if ( $this->is_subscriptions_enabled() && $this->is_changing_payment_method_for_subscription() ) {
315 $stripe_params['isChangingPayment'] = true;
316 $stripe_params['addPaymentReturnURL'] = esc_url_raw( home_url( add_query_arg( [] ) ) );
317
318 if ( $this->is_setup_intent_success_creation_redirection() && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wpnonce'] ) ) ) ) {
319 $setup_intent_id = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : '';
320 $token = $this->create_token_from_setup_intent( $setup_intent_id, wp_get_current_user() );
321 $stripe_params['newTokenFormId'] = '#wc-' . $token->get_gateway_id() . '-payment-token-' . $token->get_id();
322 }
323 return $stripe_params;
324 }
325
326 $order_id = absint( get_query_var( 'order-pay' ) );
327 $stripe_params['orderId'] = $order_id;
328 $stripe_params['isOrderPay'] = true;
329 $order = wc_get_order( $order_id );
330
331 if ( is_a( $order, 'WC_Order' ) ) {
332 $stripe_params['orderReturnURL'] = esc_url_raw(
333 add_query_arg(
334 [
335 'order_id' => $order_id,
336 'wc_payment_method' => self::ID,
337 '_wpnonce' => wp_create_nonce( 'wc_stripe_process_redirect_order_nonce' ),
338 ],
339 $this->get_return_url( $order )
340 )
341 );
342 }
343 }
344
345 // Pre-orders and free trial subscriptions don't require payments.
346 $stripe_params['isPaymentNeeded'] = $this->is_payment_needed( isset( $order_id ) ? $order_id : null );
347
348 return array_merge( $stripe_params, WC_Stripe_Helper::get_localized_messages() );
349 }
350
351 /**
352 * Gets payment method settings to pass to client scripts
353 *
354 * @return array
355 */
356 private function get_enabled_payment_method_config() {
357 $settings = [];
358 $enabled_payment_methods = $this->get_upe_enabled_at_checkout_payment_method_ids();
359
360 foreach ( $enabled_payment_methods as $payment_method ) {
361 $settings[ $payment_method ] = [
362 'isReusable' => $this->payment_methods[ $payment_method ]->is_reusable(),
363 ];
364 }
365
366 return $settings;
367 }
368
369 /**
370 * Returns the list of enabled payment method types for UPE.
371 *
372 * @return string[]
373 */
374 public function get_upe_enabled_payment_method_ids() {
375 return $this->get_option( 'upe_checkout_experience_accepted_payments', [ 'card' ] );
376 }
377
378 /**
379 * Returns the list of enabled payment method types that will function with the current checkout.
380 *
381 * @param int|null $order_id
382 * @return string[]
383 */
384 public function get_upe_enabled_at_checkout_payment_method_ids( $order_id = null ) {
385 $is_automatic_capture_enabled = $this->is_automatic_capture_enabled();
386 $available_method_ids = [];
387 foreach ( $this->get_upe_enabled_payment_method_ids() as $payment_method_id ) {
388 if ( ! isset( $this->payment_methods[ $payment_method_id ] ) ) {
389 continue;
390 }
391
392 $method = $this->payment_methods[ $payment_method_id ];
393 if ( $method->is_enabled_at_checkout( $order_id ) === false ) {
394 continue;
395 }
396
397 if ( ! $is_automatic_capture_enabled && $method->requires_automatic_capture() ) {
398 continue;
399 }
400
401 $available_method_ids[] = $payment_method_id;
402 }
403
404 return $available_method_ids;
405 }
406
407 /**
408 * Returns the list of available payment method types for UPE.
409 * See https://stripe.com/docs/stripe-js/payment-element#web-create-payment-intent for a complete list.
410 *
411 * @return string[]
412 */
413 public function get_upe_available_payment_methods() {
414 $available_payment_methods = [];
415
416 foreach ( $this->payment_methods as $payment_method ) {
417 if ( ! $payment_method->is_available() ) {
418 continue;
419 }
420 $available_payment_methods[] = $payment_method->get_id();
421 }
422 return $available_payment_methods;
423 }
424
425 /**
426 * Renders the UPE input fields needed to get the user's payment information on the checkout page
427 */
428 public function payment_fields() {
429 try {
430 $display_tokenization = $this->supports( 'tokenization' ) && is_checkout();
431
432 // Output the form HTML.
433 ?>
434 <?php if ( ! empty( $this->get_description() ) ) : ?>
435 <p><?php echo wp_kses_post( $this->get_description() ); ?></p>
436 <?php endif; ?>
437
438 <?php if ( $this->testmode ) : ?>
439 <p class="testmode-info">
440 <?php
441 printf(
442 /* translators: 1) HTML strong open tag 2) HTML strong closing tag 3) HTML anchor open tag 2) HTML anchor closing tag */
443 esc_html__( '%1$sTest mode:%2$s use the test VISA card 4242424242424242 with any expiry date and CVC. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed %3$shere%4$s.', 'woocommerce-gateway-stripe' ),
444 '<strong>',
445 '</strong>',
446 '<a href="https://stripe.com/docs/testing" target="_blank">',
447 '</a>'
448 );
449 ?>
450 </p>
451 <?php endif; ?>
452
453 <?php
454 if ( $display_tokenization ) {
455 $this->tokenization_script();
456 $this->saved_payment_methods();
457 }
458 ?>
459
460 <fieldset id="wc-stripe-upe-form" class="wc-upe-form wc-payment-form">
461 <div id="wc-stripe-upe-element"></div>
462 <div id="wc-stripe-upe-errors" role="alert"></div>
463 <input id="wc-stripe-payment-method-upe" type="hidden" name="wc-stripe-payment-method-upe" />
464 <input id="wc_stripe_selected_upe_payment_type" type="hidden" name="wc_stripe_selected_upe_payment_type" />
465 </fieldset>
466 <?php
467 $methods_enabled_for_saved_payments = array_filter( $this->get_upe_enabled_payment_method_ids(), [ $this, 'is_enabled_for_saved_payments' ] );
468 if ( $this->is_saved_cards_enabled() && ! empty( $methods_enabled_for_saved_payments ) ) {
469 $force_save_payment = ( $display_tokenization && ! apply_filters( 'wc_stripe_display_save_payment_method_checkbox', $display_tokenization ) ) || is_add_payment_method_page();
470 if ( is_user_logged_in() ) {
471 $this->save_payment_method_checkbox( $force_save_payment );
472 }
473 }
474 } catch ( Exception $e ) {
475 // Output the error message.
476 WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
477 ?>
478 <div>
479 <?php
480 echo esc_html__( 'An error was encountered when preparing the payment form. Please try again later.', 'woocommerce-gateway-stripe' );
481 ?>
482 </div>
483 <?php
484 }
485 }
486
487 /**
488 * Process the payment for a given order.
489 *
490 * @param int $order_id Reference.
491 * @param bool $retry Should we retry on fail.
492 * @param bool $force_save_source Force save the payment source.
493 * @param mix $previous_error Any error message from previous request.
494 * @param bool $use_order_source Whether to use the source, which should already be attached to the order.
495 *
496 * @return array|null An array with result of payment and redirect URL, or nothing.
497 */
498 public function process_payment( $order_id, $retry = true, $force_save_source = false, $previous_error = false, $use_order_source = false ) {
499 if ( $this->maybe_change_subscription_payment_method( $order_id ) ) {
500 return $this->process_change_subscription_payment_method( $order_id );
501 }
502
503 if ( $this->is_using_saved_payment_method() ) {
504 return $this->process_payment_with_saved_payment_method( $order_id );
505 }
506
507 $payment_intent_id = isset( $_POST['wc_payment_intent_id'] ) ? wc_clean( wp_unslash( $_POST['wc_payment_intent_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
508 $order = wc_get_order( $order_id );
509 $payment_needed = $this->is_payment_needed( $order_id );
510 $save_payment_method = $this->has_subscription( $order_id ) || ! empty( $_POST[ 'wc-' . self::ID . '-new-payment-method' ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
511 $selected_upe_payment_type = ! empty( $_POST['wc_stripe_selected_upe_payment_type'] ) ? wc_clean( wp_unslash( $_POST['wc_stripe_selected_upe_payment_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
512
513 $statement_descriptor = ! empty( $this->get_option( 'statement_descriptor' ) ) ? str_replace( "'", '', $this->get_option( 'statement_descriptor' ) ) : '';
514 $short_statement_descriptor = ! empty( $this->get_option( 'short_statement_descriptor' ) ) ? str_replace( "'", '', $this->get_option( 'short_statement_descriptor' ) ) : '';
515 $is_short_statement_descriptor_enabled = ! empty( $this->get_option( 'is_short_statement_descriptor_enabled' ) ) && 'yes' === $this->get_option( 'is_short_statement_descriptor_enabled' );
516 $descriptor = null;
517 if ( 'card' === $selected_upe_payment_type && $is_short_statement_descriptor_enabled && ! ( empty( $short_statement_descriptor ) && empty( $statement_descriptor ) ) ) {
518 // Use the shortened statement descriptor for card transactions only
519 $descriptor = WC_Stripe_Helper::get_dynamic_statement_descriptor( $short_statement_descriptor, $order, $statement_descriptor );
520 } elseif ( ! empty( $statement_descriptor ) ) {
521 $descriptor = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
522 }
523
524 if ( $payment_intent_id ) {
525 if ( $payment_needed ) {
526 $amount = $order->get_total();
527 $currency = $order->get_currency();
528 $converted_amount = WC_Stripe_Helper::get_stripe_amount( $amount, $currency );
529
530 $request = [
531 'amount' => $converted_amount,
532 'currency' => $currency,
533 'statement_descriptor' => $descriptor,
534 /* translators: 1) blog name 2) order number */
535 'description' => sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() ),
536 ];
537
538 // Get user/customer for order.
539 $customer_id = $this->get_stripe_customer_id( $order );
540 if ( ! empty( $customer_id ) ) {
541 $request['customer'] = $customer_id;
542 } else {
543 $user = $this->get_user_from_order( $order );
544 $customer = new WC_Stripe_Customer( $user->ID );
545 $request['customer'] = $customer->update_or_create_customer();// Update customer or create customer if customer does not exist.
546 }
547
548 if ( '' !== $selected_upe_payment_type ) {
549 // Only update the payment_method_types if we have a reference to the payment type the customer selected.
550 $request['payment_method_types'] = [ $selected_upe_payment_type ];
551 if ( WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID === $selected_upe_payment_type ) {
552 if ( in_array(
553 WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID,
554 $this->get_upe_enabled_payment_method_ids(),
555 true
556 ) ) {
557 $request['payment_method_types'] = [
558 WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID,
559 WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID,
560 ];
561 }
562 }
563 $this->set_payment_method_title_for_order( $order, $selected_upe_payment_type );
564 if ( ! $this->payment_methods[ $selected_upe_payment_type ]->is_allowed_on_country( $order->get_billing_country() ) ) {
565 throw new \Exception( __( 'This payment method is not available on the selected country', 'woocommerce-gateway-stripe' ) );
566 }
567 }
568
569 if ( $save_payment_method ) {
570 $request['setup_future_usage'] = 'off_session';
571 }
572
573 $request['metadata'] = $this->get_metadata_from_order( $order );
574
575 WC_Stripe_Helper::add_payment_intent_to_order( $payment_intent_id, $order );
576 $order->update_status( 'pending', __( 'Awaiting payment.', 'woocommerce-gateway-stripe' ) );
577 $order->update_meta_data( '_stripe_upe_payment_type', $selected_upe_payment_type );
578 $order->save();
579
580 $this->stripe_request(
581 "payment_intents/$payment_intent_id",
582 $request,
583 $order
584 );
585 }
586 } else {
587 return parent::process_payment( $order_id, $retry, $force_save_source, $previous_error, $use_order_source );
588 }
589
590 return [
591 'result' => 'success',
592 'payment_needed' => $payment_needed,
593 'order_id' => $order_id,
594 'redirect_url' => wp_sanitize_redirect(
595 esc_url_raw(
596 add_query_arg(
597 [
598 'order_id' => $order_id,
599 'wc_payment_method' => self::ID,
600 '_wpnonce' => wp_create_nonce( 'wc_stripe_process_redirect_order_nonce' ),
601 'save_payment_method' => $save_payment_method ? 'yes' : 'no',
602 ],
603 $this->get_return_url( $order )
604 )
605 )
606 ),
607 ];
608 }
609
610 /**
611 * Process payment using saved payment method.
612 * This follows WC_Gateway_Stripe::process_payment,
613 * but uses Payment Methods instead of Sources.
614 *
615 * @param int $order_id The order ID being processed.
616 * @param bool $can_retry Should we retry on fail.
617 */
618 public function process_payment_with_saved_payment_method( $order_id, $can_retry = true ) {
619 try {
620 $order = wc_get_order( $order_id );
621
622 if ( $this->maybe_process_pre_orders( $order_id ) ) {
623 return $this->process_pre_order( $order_id );
624 }
625
626 $token = WC_Stripe_Payment_Tokens::get_token_from_request( $_POST );
627 $payment_method = $this->stripe_request( 'payment_methods/' . $token->get_token(), [], null, 'GET' );
628 $prepared_payment_method = $this->prepare_payment_method( $payment_method );
629
630 $this->maybe_disallow_prepaid_card( $payment_method );
631 $this->save_payment_method_to_order( $order, $prepared_payment_method );
632
633 WC_Stripe_Logger::log( "Info: Begin processing payment with saved payment method for order $order_id for the amount of {$order->get_total()}" );
634
635 // If we are retrying request, maybe intent has been saved to order.
636 $intent = $this->get_intent_from_order( $order );
637
638 $enabled_payment_methods = array_filter( $this->get_upe_enabled_payment_method_ids(), [ $this, 'is_enabled_at_checkout' ] );
639 $payment_needed = $this->is_payment_needed( $order_id );
640
641 if ( $payment_needed ) {
642 // This will throw exception if not valid.
643 $this->validate_minimum_order_amount( $order );
644
645 $request_details = $this->generate_payment_request( $order, $prepared_payment_method );
646 $endpoint = false !== $intent ? "payment_intents/$intent->id" : 'payment_intents';
647 $request = [
648 'payment_method' => $payment_method->id,
649 'payment_method_types' => array_values( $enabled_payment_methods ),
650 'amount' => WC_Stripe_Helper::get_stripe_amount( $order->get_total() ),
651 'currency' => strtolower( $order->get_currency() ),
652 'description' => $request_details['description'],
653 'metadata' => $request_details['metadata'],
654 'customer' => $payment_method->customer,
655 ];
656 if ( false === $intent ) {
657 $request['capture_method'] = ( 'true' === $request_details['capture'] ) ? 'automatic' : 'manual';
658 $request['confirm'] = 'true';
659 }
660
661 $intent = $this->stripe_request(
662 $endpoint,
663 $request,
664 $order
665 );
666 } else {
667 $endpoint = false !== $intent ? "setup_intents/$intent->id" : 'setup_intents';
668 $request = [
669 'payment_method' => $payment_method->id,
670 'payment_method_types' => array_values( $enabled_payment_methods ),
671 'customer' => $payment_method->customer,
672 ];
673 if ( false === $intent ) {
674 $request['confirm'] = 'true';
675 // SEPA setup intents require mandate data.
676 if ( in_array( 'sepa_debit', array_values( $enabled_payment_methods ), true ) ) {
677 $request['mandate_data'] = [
678 'customer_acceptance' => [
679 'type' => 'online',
680 'online' => [
681 'ip_address' => WC_Geolocation::get_ip_address(),
682 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '', // @codingStandardsIgnoreLine
683 ],
684 ],
685 ];
686 }
687 }
688
689 $intent = $this->stripe_request( $endpoint, $request );
690 }
691 $this->save_intent_to_order( $order, $intent );
692
693 if ( ! empty( $intent->error ) ) {
694 $this->maybe_remove_non_existent_customer( $intent->error, $order );
695
696 // We want to retry (apparently).
697 if ( $this->is_retryable_error( $intent->error ) ) {
698 return $this->retry_after_error( $intent, $order, $can_retry );
699 }
700
701 $this->throw_localized_message( $intent, $order );
702 }
703
704 if ( 'requires_action' === $intent->status || 'requires_confirmation' === $intent->status ) {
705 if ( isset( $intent->next_action->type ) && 'redirect_to_url' === $intent->next_action->type && ! empty( $intent->next_action->redirect_to_url->url ) ) {
706 return [
707 'result' => 'success',
708 'redirect' => $intent->next_action->redirect_to_url->url,
709 ];
710 } else {
711 return [
712 'result' => 'success',
713 // Include a new nonce for update_order_status to ensure the update order
714 // status call works when a guest user creates an account during checkout.
715 'redirect' => sprintf(
716 '#wc-stripe-confirm-%s:%s:%s:%s',
717 $payment_needed ? 'pi' : 'si',
718 $order_id,
719 $intent->client_secret,
720 wp_create_nonce( 'wc_stripe_update_order_status_nonce' )
721 ),
722 ];
723 }
724 }
725
726 list( $payment_method_type, $payment_method_details ) = $this->get_payment_method_data_from_intent( $intent );
727
728 if ( $payment_needed ) {
729 // Use the last charge within the intent to proceed.
730 $this->process_response( end( $intent->charges->data ), $order );
731 } else {
732 $order->payment_complete();
733 }
734 $this->set_payment_method_title_for_order( $order, $payment_method_type );
735
736 // Remove cart.
737 if ( isset( WC()->cart ) ) {
738 WC()->cart->empty_cart();
739 }
740
741 // Return thank you page redirect.
742 return [
743 'result' => 'success',
744 'redirect' => $this->get_return_url( $order ),
745 ];
746
747 } catch ( WC_Stripe_Exception $e ) {
748 wc_add_notice( $e->getLocalizedMessage(), 'error' );
749 WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
750
751 do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
752
753 /* translators: error message */
754 $order->update_status( 'failed' );
755
756 return [
757 'result' => 'fail',
758 'redirect' => '',
759 ];
760 }
761 }
762
763 /**
764 * Check for a UPE redirect payment method on order received page or setup intent on payment methods page.
765 *
766 * @since 5.6.0
767 * @version 5.6.0
768 */
769 public function maybe_process_upe_redirect() {
770 if ( $this->is_payment_methods_page() ) {
771 if ( $this->is_setup_intent_success_creation_redirection() ) {
772 if ( isset( $_GET['redirect_status'] ) && 'succeeded' === $_GET['redirect_status'] ) {
773 $user_id = wp_get_current_user()->ID;
774 $customer = new WC_Stripe_Customer( $user_id );
775 $customer->clear_cache();
776 wc_add_notice( __( 'Payment method successfully added.', 'woocommerce-gateway-stripe' ) );
777
778 // The newly created payment method does not inherit the customers' billing info, so we manually
779 // trigger an update; in case of failure we log the error and continue because the payment method's
780 // billing info will be updated when the customer makes a purchase anyway.
781 try {
782 $setup_intent_id = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : '';
783 $setup_intent = $this->stripe_request( 'setup_intents/' . $setup_intent_id, [], null, 'GET' );
784
785 $customer_data = WC_Stripe_Customer::map_customer_data( null, new WC_Customer( $user_id ) );
786 $payment_method_object = $this->stripe_request(
787 'payment_methods/' . $setup_intent->payment_method,
788 [
789 'billing_details' => [
790 'name' => $customer_data['name'],
791 'email' => $customer_data['email'],
792 'phone' => $customer_data['phone'],
793 'address' => $customer_data['address'],
794 ],
795 ]
796 );
797
798 do_action( 'woocommerce_stripe_add_payment_method', $user_id, $payment_method_object );
799 } catch ( Exception $e ) {
800 WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
801 }
802 } else {
803 wc_add_notice( __( 'Failed to add payment method.', 'woocommerce-gateway-stripe' ), 'error', [ 'icon' => 'error' ] );
804 }
805 }
806 return;
807 }
808
809 if ( ! is_order_received_page() ) {
810 return;
811 }
812
813 $payment_method = isset( $_GET['wc_payment_method'] ) ? wc_clean( wp_unslash( $_GET['wc_payment_method'] ) ) : '';
814 if ( self::ID !== $payment_method ) {
815 return;
816 }
817
818 $is_nonce_valid = isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wpnonce'] ) ), 'wc_stripe_process_redirect_order_nonce' );
819 if ( ! $is_nonce_valid || empty( $_GET['wc_payment_method'] ) ) {
820 return;
821 }
822
823 if ( ! empty( $_GET['payment_intent_client_secret'] ) ) {
824 $intent_id = isset( $_GET['payment_intent'] ) ? wc_clean( wp_unslash( $_GET['payment_intent'] ) ) : '';
825 } elseif ( ! empty( $_GET['setup_intent_client_secret'] ) ) {
826 $intent_id = isset( $_GET['setup_intent'] ) ? wc_clean( wp_unslash( $_GET['setup_intent'] ) ) : '';
827 } else {
828 return;
829 }
830
831 $order_id = isset( $_GET['order_id'] ) ? wc_clean( wp_unslash( $_GET['order_id'] ) ) : '';
832 $save_payment_method = isset( $_GET['save_payment_method'] ) ? 'yes' === wc_clean( wp_unslash( $_GET['save_payment_method'] ) ) : false;
833
834 if ( empty( $intent_id ) || empty( $order_id ) ) {
835 return;
836 }
837
838 $this->process_upe_redirect_payment( $order_id, $intent_id, $save_payment_method );
839 }
840
841 /**
842 * Processes UPE redirect payments.
843 *
844 * @param int $order_id The order ID being processed.
845 * @param string $intent_id The Stripe setup/payment intent ID for the order payment.
846 * @param bool $save_payment_method Boolean representing whether payment method for order should be saved.
847 *
848 * @since 5.5.0
849 * @version 5.5.0
850 */
851 public function process_upe_redirect_payment( $order_id, $intent_id, $save_payment_method ) {
852 $order = wc_get_order( $order_id );
853
854 if ( ! is_object( $order ) ) {
855 return;
856 }
857
858 if ( $order->has_status( [ 'processing', 'completed', 'on-hold' ] ) ) {
859 return;
860 }
861
862 if ( $order->get_meta( '_stripe_upe_redirect_processed', true ) ) {
863 return;
864 }
865
866 WC_Stripe_Logger::log( "Begin processing UPE redirect payment for order $order_id for the amount of {$order->get_total()}" );
867
868 try {
869 $this->process_order_for_confirmed_intent( $order, $intent_id, $save_payment_method );
870 } catch ( Exception $e ) {
871 WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
872
873 /* translators: localized exception message */
874 $order->update_status( 'failed', sprintf( __( 'UPE payment failed: %s', 'woocommerce-gateway-stripe' ), $e->getMessage() ) );
875
876 wc_add_notice( $e->getMessage(), 'error' );
877 wp_safe_redirect( wc_get_checkout_url() );
878 exit;
879 }
880 }
881
882 /**
883 * Update order and maybe save payment method for an order after an intent has been created and confirmed.
884 *
885 * @param WC_Order $order Order being processed.
886 * @param string $intent_id Stripe setup/payment ID.
887 * @param bool $save_payment_method Boolean representing whether payment method for order should be saved.
888 */
889 public function process_order_for_confirmed_intent( $order, $intent_id, $save_payment_method ) {
890 $payment_needed = $this->is_payment_needed( $order->get_id() );
891
892 // Get payment intent to confirm status.
893 if ( $payment_needed ) {
894 $intent = $this->stripe_request( 'payment_intents/' . $intent_id . '?expand[]=payment_method' );
895 $error = isset( $intent->last_payment_error ) ? $intent->last_payment_error : false;
896 } else {
897 $intent = $this->stripe_request( 'setup_intents/' . $intent_id . '?expand[]=payment_method&expand[]=latest_attempt' );
898 $error = isset( $intent->last_setup_error ) ? $intent->last_setup_error : false;
899 }
900
901 if ( ! empty( $error ) ) {
902 WC_Stripe_Logger::log( 'Error when processing payment: ' . $error->message );
903 throw new WC_Stripe_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
904 }
905
906 list( $payment_method_type, $payment_method_details ) = $this->get_payment_method_data_from_intent( $intent );
907
908 if ( ! isset( $this->payment_methods[ $payment_method_type ] ) ) {
909 return;
910 }
911 $payment_method = $this->payment_methods[ $payment_method_type ];
912
913 if ( $this->maybe_process_pre_orders( $order->get_id() ) ) {
914 // If this is a pre-order, simply mark the order as pre-ordered and allow
915 // the subsequent logic to save the payment method and proceed to complete the order.
916 $this->mark_order_as_pre_ordered( $order->get_id() );
917 $save_payment_method = true;
918 }
919
920 if ( $save_payment_method && $payment_method->is_reusable() ) {
921 $payment_method_object = null;
922 if ( $payment_method->get_id() !== $payment_method->get_retrievable_type() ) {
923 $generated_payment_method_id = $payment_method_details[ $payment_method_type ]->generated_sepa_debit;
924 $payment_method_object = $this->stripe_request( "payment_methods/$generated_payment_method_id", [], null, 'GET' );
925 } else {
926 $payment_method_object = $intent->payment_method;
927 }
928 $user = $this->get_user_from_order( $order );
929 $customer = new WC_Stripe_Customer( $user->ID );
930 $prepared_payment_method = $this->prepare_payment_method( $payment_method_object );
931
932 $customer->clear_cache();
933 $this->save_payment_method_to_order( $order, $prepared_payment_method );
934 do_action( 'woocommerce_stripe_add_payment_method', $user->get_id(), $payment_method_object );
935 }
936
937 if ( $payment_needed ) {
938 // Use the last charge within the intent to proceed.
939 $this->process_response( end( $intent->charges->data ), $order );
940 } else {
941 $order->payment_complete();
942 }
943 $this->save_intent_to_order( $order, $intent );
944 $this->set_payment_method_title_for_order( $order, $payment_method_type );
945 $order->update_meta_data( '_stripe_upe_redirect_processed', true );
946 $order->save();
947 }
948
949 /**
950 * Converts payment method into object similar to prepared source
951 * compatible with wc_stripe_payment_metadata and wc_stripe_generate_payment_request filters.
952 *
953 * @param object $payment_method Stripe payment method object response.
954 *
955 * @return object
956 */
957 public function prepare_payment_method( $payment_method ) {
958 return (object) [
959 'customer' => $payment_method->customer,
960 'source' => null,
961 'source_object' => null,
962 'payment_method' => $payment_method->id,
963 'payment_method_object' => $payment_method,
964 ];
965 }
966
967 /**
968 * Save payment method to order.
969 *
970 * @param WC_Order $order For to which the source applies.
971 * @param stdClass $payment_method Stripe Payment Method.
972 */
973 public function save_payment_method_to_order( $order, $payment_method ) {
974 if ( $payment_method->customer ) {
975 $order->update_meta_data( '_stripe_customer_id', $payment_method->customer );
976 }
977 // Save the payment method id as `source_id`, because we use both `sources` and `payment_methods` APIs.
978 $order->update_meta_data( '_stripe_source_id', $payment_method->payment_method );
979
980 if ( is_callable( [ $order, 'save' ] ) ) {
981 $order->save();
982 }
983
984 $this->maybe_update_source_on_subscription_order( $order, $payment_method );
985 }
986
987 /**
988 * Retries the payment process once an error occured.
989 *
990 * @param object $intent The Payment Intent response from the Stripe API.
991 * @param WC_Order $order An order that is being paid for.
992 * @param bool $retry A flag that indicates whether another retry should be attempted.
993 * @param bool $force_save_source Force save the payment source.
994 * @param mixed $previous_error Any error message from previous request.
995 * @param bool $use_order_source Whether to use the source, which should already be attached to the order.
996 * @throws WC_Stripe_Exception If the payment is not accepted.
997 * @return array|void
998 */
999 public function retry_after_error( $intent, $order, $retry, $force_save_source = false, $previous_error = false, $use_order_source = false ) {
1000 if ( ! $retry ) {
1001 $localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
1002 $order->add_order_note( $localized_message );
1003 throw new WC_Stripe_Exception( print_r( $intent, true ), $localized_message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.
1004 }
1005
1006 // Don't do anymore retries after this.
1007 if ( 5 <= $this->retry_interval ) {
1008 return $this->process_payment_with_saved_payment_method( $order->get_id(), false );
1009 }
1010
1011 sleep( $this->retry_interval );
1012 $this->retry_interval++;
1013
1014 return $this->process_payment_with_saved_payment_method( $order->get_id(), true );
1015 }
1016
1017 /**
1018 * Returns true if a payment is needed for the current cart or order.
1019 * Pre-Orders and Subscriptions may not require an upfront payment, so we need to check whether
1020 * or not the payment is necessary to decide for either a setup intent or a payment intent.
1021 *
1022 * @since 5.8.0
1023 *
1024 * @param int $order_id The order ID being processed.
1025 *
1026 * @return bool Whether a payment is necessary.
1027 */
1028 public function is_payment_needed( $order_id = null ) {
1029 if ( $this->is_pre_order_item_in_cart() || ( ! empty( $order_id ) && $this->has_pre_order( $order_id ) ) ) {
1030 $pre_order_product = ( ! empty( $order_id ) ) ? $this->get_pre_order_product_from_order( $order_id ) : $this->get_pre_order_product_from_cart();
1031 // Only one pre-order product is allowed per cart,
1032 // so we can return if it's charged upfront.
1033 return $this->is_pre_order_product_charged_upfront( $pre_order_product );
1034 }
1035
1036 // Free trial subscriptions without a sign up fee, or any other type
1037 // of order with a `0` amount should fall into the logic below.
1038 $amount = is_null( WC()->cart ) ? 0 : WC()->cart->get_total( false );
1039 $order = isset( $order_id ) ? wc_get_order( $order_id ) : null;
1040 if ( is_a( $order, 'WC_Order' ) ) {
1041 $amount = $order->get_total();
1042 }
1043
1044 $converted_amount = WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( get_woocommerce_currency() ) );
1045
1046 return 0 < $converted_amount;
1047 }
1048
1049 /**
1050 * Checks if card on Payment Method is a prepaid card.
1051 *
1052 * @since 4.0.6
1053 * @param object $payment_method
1054 * @return bool
1055 */
1056 public function is_prepaid_card( $payment_method ) {
1057 return (
1058 $payment_method
1059 && ( 'card' === $payment_method->type )
1060 && 'prepaid' === $payment_method->card->funding
1061 );
1062 }
1063
1064 /**
1065 * Get WC User from WC Order.
1066 *
1067 * @param WC_Order $order
1068 *
1069 * @return WP_User
1070 */
1071 public function get_user_from_order( $order ) {
1072 $user = $order->get_user();
1073 if ( false === $user ) {
1074 $user = wp_get_current_user();
1075 }
1076 return $user;
1077 }
1078
1079 /**
1080 * Checks if gateway should be available to use.
1081 *
1082 * @since 5.6.0
1083 */
1084 public function is_available() {
1085 $methods_enabled_for_saved_payments = array_filter( $this->get_upe_enabled_payment_method_ids(), [ $this, 'is_enabled_for_saved_payments' ] );
1086 if ( is_add_payment_method_page() && count( $methods_enabled_for_saved_payments ) === 0 ) {
1087 return false;
1088 }
1089
1090 return parent::is_available();
1091 }
1092
1093 /**
1094 * Function to be used with array_filter
1095 * to filter UPE payment methods supported with current checkout
1096 *
1097 * @param string $payment_method_id Stripe payment method.
1098 *
1099 * @return bool
1100 */
1101 public function is_enabled_at_checkout( $payment_method_id ) {
1102 if ( ! isset( $this->payment_methods[ $payment_method_id ] ) ) {
1103 return false;
1104 }
1105 return $this->payment_methods[ $payment_method_id ]->is_enabled_at_checkout();
1106 }
1107
1108 /**
1109 * Function to be used with array_filter
1110 * to filter UPE payment methods that support saved payments
1111 *
1112 * @param string $payment_method_id Stripe payment method.
1113 *
1114 * @return bool
1115 */
1116 public function is_enabled_for_saved_payments( $payment_method_id ) {
1117 if ( ! isset( $this->payment_methods[ $payment_method_id ] ) ) {
1118 return false;
1119 }
1120 return $this->payment_methods[ $payment_method_id ]->is_reusable();
1121 }
1122
1123 // TODO: Actually validate.
1124 public function validate_upe_checkout_experience_accepted_payments_field( $key, $value ) {
1125 return $value;
1126 }
1127
1128 /**
1129 * Checks if the setting to allow the user to save cards is enabled.
1130 *
1131 * @return bool Whether the setting to allow saved cards is enabled or not.
1132 */
1133 public function is_saved_cards_enabled() {
1134 return $this->saved_cards;
1135 }
1136
1137 /**
1138 * Set formatted readable payment method title for order,
1139 * using payment method details from accompanying charge.
1140 *
1141 * @param WC_Order $order WC Order being processed.
1142 * @param string $payment_method_type Stripe payment method key.
1143 *
1144 * @since 5.5.0
1145 * @version 5.5.0
1146 */
1147 public function set_payment_method_title_for_order( $order, $payment_method_type ) {
1148 if ( ! isset( $this->payment_methods[ $payment_method_type ] ) ) {
1149 return;
1150 }
1151
1152 $payment_method_title = $this->payment_methods[ $payment_method_type ]->get_label();
1153
1154 $order->set_payment_method( self::ID );
1155 $order->set_payment_method_title( $payment_method_title );
1156 $order->save();
1157 }
1158
1159 /**
1160 * This is overloading the upe checkout experience type on the settings page.
1161 *
1162 * @param string $key Field key.
1163 * @param array $data Field data.
1164 * @return string
1165 */
1166 public function generate_upe_checkout_experience_accepted_payments_html( $key, $data ) {
1167 $stripe_account = $this->stripe_request( 'account' );
1168 $stripe_capabilities = isset( $stripe_account->capabilities ) ? (array) $stripe_account->capabilities : [];
1169 $data['description'] = '<p>' . __( "Select payments available to customers at checkout. We'll only show your customers the most relevant payment methods based on their currency and location.", 'woocommerce-gateway-stripe' ) . '</p>
1170 <table class="wc_gateways widefat form-table wc-stripe-upe-method-selection" cellspacing="0" aria-describedby="wc_stripe_upe_method_selection">
1171 <thead>
1172 <tr>
1173 <th class="name wc-stripe-upe-method-selection__name">' . esc_html__( 'Method', 'woocommerce-gateway-stripe' ) . '</th>
1174 <th class="status wc-stripe-upe-method-selection__status">' . esc_html__( 'Enabled', 'woocommerce-gateway-stripe' ) . '</th>
1175 <th class="description wc-stripe-upe-method-selection__description">' . esc_html__( 'Description', 'woocommerce-gateway-stripe' ) . '</th>
1176 </tr>
1177 </thead>
1178 <tbody>';
1179
1180 $is_automatic_capture_enabled = $this->is_automatic_capture_enabled();
1181
1182 foreach ( $this->payment_methods as $method_id => $method ) {
1183 $method_enabled = in_array( $method_id, $this->get_upe_enabled_payment_method_ids(), true ) && ( $is_automatic_capture_enabled || ! $method->requires_automatic_capture() ) ? 'enabled' : 'disabled';
1184 $method_enabled_label = 'enabled' === $method_enabled ? __( 'enabled', 'woocommerce-gateway-stripe' ) : __( 'disabled', 'woocommerce-gateway-stripe' );
1185 $capability_id = "{$method_id}_payments"; // "_payments" is a suffix that comes from Stripe API, except when it is "transfers", which does not apply here
1186 $method_status = isset( $stripe_capabilities[ $capability_id ] ) ? $stripe_capabilities[ $capability_id ] : 'inactive';
1187 $subtext_messages = $method->get_subtext_messages( $method_status );
1188 $aria_label = sprintf(
1189 /* translators: $1%s payment method ID, $2%s "enabled" or "disabled" */
1190 esc_attr__( 'The &quot;%1$s&quot; payment method is currently %2$s', 'woocommerce-gateway-stripe' ),
1191 $method_id,
1192 $method_enabled_label
1193 );
1194 $manual_capture_tip = sprintf(
1195 /* translators: $1%s payment method label */
1196 __( '%1$s is not available to your customers when manual capture is enabled.', 'woocommerce-gateway-stripe' ),
1197 $method->get_label()
1198 );
1199 $data['description'] .= '<tr data-upe_method_id="' . $method_id . '">
1200 <td class="name wc-stripe-upe-method-selection__name" width="">
1201 ' . $method->get_label() . '
1202 ' . ( empty( $subtext_messages ) ? '' : '<span class="wc-payment-gateway-method-name">&nbsp;–&nbsp;' . $subtext_messages . '</span>' ) . '
1203 </td>
1204 <td class="status wc-stripe-upe-method-selection__status" width="1%">
1205 <a class="wc-payment-upe-method-toggle-' . $method_enabled . '" href="#">
1206 <span class="woocommerce-input-toggle woocommerce-input-toggle--' . $method_enabled . '" aria-label="' . $aria_label . '">
1207 ' . ( 'enabled' === $method_enabled ? __( 'Yes', 'woocommerce-gateway-stripe' ) : __( 'No', 'woocommerce-gateway-stripe' ) ) . '
1208 </span>
1209 </a>'
1210 . ( ! $is_automatic_capture_enabled && $method->requires_automatic_capture() ? '<span class="tips dashicons dashicons-warning" style="margin-top: 1px; margin-right: -25px; margin-left: 5px; color: red" data-tip="' . $manual_capture_tip . '" />' : '' ) .
1211 '</td>
1212 <td class="description wc-stripe-upe-method-selection__description" width="">' . $method->get_description() . '</td>
1213 </tr>';
1214 }
1215
1216 $data['description'] .= '</tbody>
1217 </table>
1218 <p><a class="button" target="_blank" href="https://dashboard.stripe.com/account/payments/settings">' . __( 'Get more payment methods', 'woocommerce-gateway-stripe' ) . '</a></p>
1219 <span id="wc_stripe_upe_change_notice" class="hidden">' . __( 'You must save your changes.', 'woocommerce-gateway-stripe' ) . '</span>';
1220
1221 return $this->generate_title_html( $key, $data );
1222 }
1223
1224 /**
1225 * Extacts the Stripe intent's payment_method_type and payment_method_details values.
1226 *
1227 * @param $intent Stripe's intent response.
1228 * @return string[] List with 2 values: payment_method_type and payment_method_details.
1229 */
1230 private function get_payment_method_data_from_intent( $intent ) {
1231 $payment_method_type = '';
1232 $payment_method_details = false;
1233
1234 if ( 'payment_intent' === $intent->object ) {
1235 if ( ! empty( $intent->charges ) && 0 < $intent->charges->total_count ) {
1236 $charge = end( $intent->charges->data );
1237 $payment_method_details = (array) $charge->payment_method_details;
1238 $payment_method_type = ! empty( $payment_method_details ) ? $payment_method_details['type'] : '';
1239 }
1240 } elseif ( 'setup_intent' === $intent->object ) {
1241 if ( ! empty( $intent->latest_attempt ) && ! empty( $intent->latest_attempt->payment_method_details ) ) {
1242 $payment_method_details = (array) $intent->latest_attempt->payment_method_details;
1243 $payment_method_type = $payment_method_details['type'];
1244 } elseif ( ! empty( $intent->payment_method ) ) {
1245 $payment_method_details = $intent->payment_method;
1246 $payment_method_type = $payment_method_details->type;
1247 }
1248 // Setup intents don't have details, keep the false value.
1249 }
1250
1251 return [ $payment_method_type, $payment_method_details ];
1252 }
1253
1254 /**
1255 * Prepares Stripe metadata for a given order.
1256 *
1257 * @param WC_Order $order Order being processed.
1258 *
1259 * @return array Array of keyed metadata values.
1260 */
1261 public function get_metadata_from_order( $order ) {
1262 $payment_type = $this->is_payment_recurring( $order->get_id() ) ? 'recurring' : 'single';
1263 $name = sanitize_text_field( $order->get_billing_first_name() ) . ' ' . sanitize_text_field( $order->get_billing_last_name() );
1264 $email = sanitize_email( $order->get_billing_email() );
1265
1266 return [
1267 'customer_name' => $name,
1268 'customer_email' => $email,
1269 'site_url' => esc_url( get_site_url() ),
1270 'order_id' => $order->get_id(),
1271 'order_key' => $order->get_order_key(),
1272 'payment_type' => $payment_type,
1273 ];
1274 }
1275
1276 /**
1277 * Returns true when viewing payment methods page.
1278 *
1279 * @return bool
1280 */
1281 private function is_payment_methods_page() {
1282 global $wp;
1283
1284 $page_id = wc_get_page_id( 'myaccount' );
1285
1286 return ( $page_id && is_page( $page_id ) && ( isset( $wp->query_vars['payment-methods'] ) ) );
1287 }
1288
1289 /**
1290 * True if the request contains the values that indicates a redirection after a successful setup intent creation.
1291 *
1292 * @return bool
1293 */
1294 private function is_setup_intent_success_creation_redirection() {
1295 return ( ! empty( $_GET['setup_intent_client_secret'] ) & ! empty( $_GET['setup_intent'] ) & ! empty( $_GET['redirect_status'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1296 }
1297
1298 /**
1299 * Adds a token to current user from a setup intent id.
1300 *
1301 * @param string $setup_intent_id ID of the setup intent.
1302 * @param WP_User $user User to add token to.
1303 *
1304 * @return WC_Payment_Token_CC|WC_Payment_Token_WCPay_SEPA The added token.
1305 *
1306 * @since 5.8.0
1307 * @version 5.8.0
1308 */
1309 public function create_token_from_setup_intent( $setup_intent_id, $user ) {
1310 try {
1311 $setup_intent = $this->stripe_request( 'setup_intents/' . $setup_intent_id );
1312 if ( ! empty( $setup_intent->last_payment_error ) ) {
1313 throw new WC_Stripe_Exception( __( "We're not able to add this payment method. Please try again later.", 'woocommerce-gateway-stripe' ) );
1314 }
1315
1316 $payment_method_id = $setup_intent->payment_method;
1317 $payment_method_object = $this->stripe_request( 'payment_methods/' . $payment_method_id );
1318
1319 $payment_method = $this->payment_methods[ $payment_method_object->type ];
1320
1321 $customer = new WC_Stripe_Customer( wp_get_current_user()->ID );
1322 $customer->clear_cache();
1323
1324 return $payment_method->create_payment_token_for_user( $user->ID, $payment_method_object );
1325 } catch ( Exception $e ) {
1326 wc_add_notice( $e->getMessage(), 'error', [ 'icon' => 'error' ] );
1327 WC_Stripe_Logger::log( 'Error when adding payment method: ' . $e->getMessage() );
1328 return [
1329 'result' => 'error',
1330 ];
1331 }
1332 }
1333
1334 /**
1335 * Wrapper function to manage requests to WC_Stripe_API.
1336 *
1337 * @param string $path Stripe API endpoint path to query.
1338 * @param string $params Parameters for request body.
1339 * @param WC_Order $order WC Order for request.
1340 * @param string $method HTTP method for request.
1341 *
1342 * @return object JSON response object.
1343 */
1344 protected function stripe_request( $path, $params = null, $order = null, $method = 'POST' ) {
1345 if ( is_null( $params ) ) {
1346 return WC_Stripe_API::retrieve( $path );
1347 }
1348 if ( ! is_null( $order ) ) {
1349 $level3_data = $this->get_level3_data_from_order( $order );
1350 return WC_Stripe_API::request_with_level3_data( $params, $path, $level3_data, $order );
1351 }
1352 return WC_Stripe_API::request( $params, $path, $method );
1353 }
1354
1355}