Plugin Directory

source: woocommerce-gateway-stripe/trunk/includes/class-wc-stripe-intent-controller.php

Last change on this file was 3393884, checked in by wesleyjrosa, 10 days ago

Tagging version 10.1.0

  • Property svn:executable set to *
File size: 54.8 KB
Line 
1<?php
2
3use Automattic\WooCommerce\Enums\OrderStatus;
4
5if ( ! defined( 'ABSPATH' ) ) {
6        exit;
7}
8
9/**
10 * WC_Stripe_Intent_Controller class.
11 *
12 * Handles in-checkout AJAX calls, related to Payment Intents.
13 */
14class WC_Stripe_Intent_Controller {
15        /**
16         * Holds an instance of the gateway class.
17         *
18         * @since 4.2.0
19         * @var WC_Gateway_Stripe
20         */
21        protected $gateway;
22
23        /**
24         * Adds the necessary hooks.
25         *
26         * @since 4.2.0
27         */
28        public function init_hooks() {
29                add_action( 'wc_ajax_wc_stripe_verify_intent', [ $this, 'verify_intent' ] );
30                add_action( 'wc_ajax_wc_stripe_create_setup_intent', [ $this, 'create_setup_intent' ] );
31
32                // Use wp_ajax instead of wc_ajax to ensure only logged in users can fire this action.
33                add_action( 'wp_ajax_wc_stripe_create_and_confirm_setup_intent', [ $this, 'create_and_confirm_setup_intent_ajax' ] );
34
35                add_action( 'wc_ajax_wc_stripe_create_payment_intent', [ $this, 'create_payment_intent_ajax' ] );
36                add_action( 'wc_ajax_wc_stripe_update_payment_intent', [ $this, 'update_payment_intent_ajax' ] );
37                add_action( 'wc_ajax_wc_stripe_init_setup_intent', [ $this, 'init_setup_intent_ajax' ] );
38
39                add_action( 'wc_ajax_wc_stripe_update_order_status', [ $this, 'update_order_status_ajax' ] );
40                add_action( 'wc_ajax_wc_stripe_update_failed_order', [ $this, 'update_failed_order_ajax' ] );
41
42                add_action( 'wc_ajax_wc_stripe_confirm_change_payment', [ $this, 'confirm_change_payment_from_setup_intent_ajax' ] );
43        }
44
45        /**
46         * Returns an instantiated gateway.
47         *
48         * @since 4.2.0
49         * @return WC_Stripe_Payment_Gateway
50         */
51        protected function get_gateway() {
52                if ( ! isset( $this->gateway ) ) {
53                        $gateways      = WC()->payment_gateways()->payment_gateways();
54                        $this->gateway = $gateways[ WC_Stripe_UPE_Payment_Gateway::ID ];
55                }
56
57                return $this->gateway;
58        }
59
60        /**
61         * Returns an instantiated UPE gateway
62         *
63         * @since 5.6.0
64         * @throws WC_Stripe_Exception if UPE is not enabled.
65         * @return WC_Stripe_UPE_Payment_Gateway
66         */
67        protected function get_upe_gateway() {
68                $gateway = $this->get_gateway();
69                if ( ! $gateway instanceof WC_Stripe_UPE_Payment_Gateway ) {
70                        WC_Stripe_Logger::log( 'Error instantiating the UPE Payment Gateway, UPE is not enabled.' );
71                        throw new WC_Stripe_Exception( __( "We're not able to process this payment.", 'woocommerce-gateway-stripe' ) );
72                }
73                return $gateway;
74        }
75
76        /**
77         * Loads the order from the current request.
78         *
79         * @since 4.2.0
80         * @throws WC_Stripe_Exception An exception if there is no order ID or the order does not exist.
81         * @return WC_Order
82         */
83        private function get_order_from_request() {
84                if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['nonce'] ), 'wc_stripe_confirm_pi' ) ) {
85                        throw new WC_Stripe_Exception( 'missing-nonce', __( 'CSRF verification failed.', 'woocommerce-gateway-stripe' ) );
86                }
87
88                // Load the order ID.
89                $order_id = null;
90                if ( isset( $_GET['order'] ) && absint( $_GET['order'] ) ) {
91                        $order_id = absint( $_GET['order'] );
92                }
93
94                // Retrieve the order.
95                $order = wc_get_order( $order_id );
96
97                if ( ! $order ) {
98                        throw new WC_Stripe_Exception( 'missing-order', __( 'Missing order ID for payment confirmation', 'woocommerce-gateway-stripe' ) );
99                }
100
101                return $order;
102        }
103
104        /**
105         * Handles successful PaymentIntent authentications.
106         *
107         * @since 4.2.0
108         */
109        public function verify_intent() {
110                global $woocommerce;
111
112                $order   = false;
113                $gateway = $this->get_gateway();
114
115                try {
116                        $order = $this->get_order_from_request();
117
118                        // Validate order status.
119                        if ( ! $order->has_status(
120                                apply_filters(
121                                        'wc_stripe_allowed_payment_processing_statuses',
122                                        [ OrderStatus::PENDING, OrderStatus::FAILED ],
123                                        $order
124                                )
125                        ) ) {
126                                throw new WC_Stripe_Exception( 'invalid_order_status', __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
127                        }
128
129                        // Validate the intent being verified.
130                        $order_intent_id = WC_Stripe_Order_Helper::get_instance()->get_stripe_intent_id( $order );
131                        if ( ! $order_intent_id || ! isset( $_GET['intent_id'] ) || $order_intent_id !== $_GET['intent_id'] ) {
132                                throw new WC_Stripe_Exception( 'invalid_intent', __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
133                        }
134                } catch ( WC_Stripe_Exception $e ) {
135                        // Unset the order.
136                        $order = null;
137
138                        /* translators: Error message text */
139                        $message = sprintf( __( 'Payment verification error: %s', 'woocommerce-gateway-stripe' ), $e->getLocalizedMessage() );
140                        wc_add_notice( esc_html( $message ), 'error' );
141
142                        $redirect_url = $woocommerce->cart->is_empty()
143                                ? get_permalink( wc_get_page_id( 'shop' ) )
144                                : wc_get_checkout_url();
145
146                        $this->handle_error( $e, $redirect_url );
147                }
148
149                try {
150                        $gateway->verify_intent_after_checkout( $order );
151
152                        if ( isset( $_GET['save_payment_method'] ) && ! empty( $_GET['save_payment_method'] ) ) {
153                                $intent = $gateway->get_intent_from_order( $order );
154                                if ( isset( $intent->last_payment_error ) ) {
155                                        $last_payment_error = $intent->last_payment_error;
156                                        $source_id          = '';
157
158                                        // Backwards compatibility for payment intents that use sources.
159                                        if ( isset( $last_payment_error->payment_method->id ) ) {
160                                                $source_id = $last_payment_error->payment_method->id;
161                                        } elseif ( isset( $last_payment_error->source->id ) ) {
162                                                $source_id = $last_payment_error->source->id;
163                                        }
164
165                                        // Currently, Stripe saves the payment method even if the authentication fails for 3DS cards.
166                                        // Although, the card is not stored in DB we need to remove the source from the customer on Stripe
167                                        // in order to keep the sources in sync with the data in DB.
168                                        if ( ! empty( $source_id ) ) {
169                                                $customer = new WC_Stripe_Customer( wp_get_current_user()->ID );
170                                                $customer->delete_source( $source_id );
171                                        }
172                                } else {
173                                        $metadata = $intent->metadata;
174                                        if ( isset( $metadata->save_payment_method ) && 'true' === $metadata->save_payment_method ) {
175                                                $payment_method = WC_Stripe_Helper::get_payment_method_from_intent( $intent );
176                                                $source_object  = WC_Stripe_API::get_payment_method(
177                                                        // The object on the intent may have been expanded so we need to check if it's just the ID or the full object.
178                                                        is_string( $payment_method ) ? $payment_method : $payment_method->id
179                                                );
180                                                $gateway->save_payment_method( $source_object );
181                                        }
182                                }
183                        }
184
185                        if ( ! isset( $_GET['is_ajax'] ) ) {
186                                $redirect_url = isset( $_GET['redirect_to'] ) // wpcs: csrf ok.
187                                        ? esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) // wpcs: csrf ok.
188                                        : $gateway->get_return_url( $order );
189
190                                wp_safe_redirect( $redirect_url );
191                        }
192
193                        exit;
194                } catch ( WC_Stripe_Exception $e ) {
195                        $this->handle_error( $e, $gateway->get_return_url( $order ) );
196                }
197        }
198
199        /**
200         * Handles exceptions during intent verification.
201         *
202         * @since 4.2.0
203         * @param WC_Stripe_Exception $e           The exception that was thrown.
204         * @param string              $redirect_url An URL to use if a redirect is needed.
205         */
206        protected function handle_error( $e, $redirect_url ) {
207                // Log the exception before redirecting.
208                $message = sprintf( 'PaymentIntent verification exception: %s', $e->getLocalizedMessage() );
209                WC_Stripe_Logger::log( $message );
210
211                // `is_ajax` is only used for PI error reporting, a response is not expected.
212                if ( isset( $_GET['is_ajax'] ) ) {
213                        exit;
214                }
215
216                wp_safe_redirect( $redirect_url );
217                exit;
218        }
219
220        /**
221         * Creates a Setup Intent through AJAX while adding cards.
222         */
223        public function create_setup_intent() {
224                if (
225                        ! is_user_logged_in()
226                        || ! isset( $_POST['stripe_source_id'] )
227                        || ! isset( $_POST['nonce'] )
228                ) {
229                        return;
230                }
231
232                // similar rate limiter is present in WC Core, but it's executed on page submission (and not on AJAX calls).
233                $wc_add_payment_method_rate_limit_id = 'add_payment_method_' . get_current_user_id();
234                if ( WC_Rate_Limiter::retried_too_soon( $wc_add_payment_method_rate_limit_id ) ) {
235                        echo wp_json_encode(
236                                [
237                                        'status' => 'error',
238                                        'error'  => [
239                                                'type'    => 'setup_intent_error',
240                                                'message' => __( 'Failed to save payment method.', 'woocommerce-gateway-stripe' ),
241                                        ],
242                                ]
243                        );
244                        exit;
245                }
246
247                try {
248                        $source_id = wc_clean( wp_unslash( $_POST['stripe_source_id'] ) );
249
250                        // 1. Verify.
251                        if (
252                                ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wc_stripe_create_si' )
253                                || ! ( 0 === strpos( $source_id, 'src_' ) || 0 === strpos( $source_id, 'pm_' ) )
254                        ) {
255                                throw new Exception( __( 'Unable to verify your request. Please reload the page and try again.', 'woocommerce-gateway-stripe' ) );
256                        }
257
258                        // 2. Load the customer ID (and create a customer eventually).
259                        $customer = new WC_Stripe_Customer( wp_get_current_user()->ID );
260                        $customer->maybe_create_customer();
261
262                        // 3. Fetch the source object.
263                        $source_object = WC_Stripe_API::get_payment_method( $source_id );
264
265                        if ( ! empty( $source_object->error ) ) {
266                                throw new Exception( $source_object->error->message );
267                        }
268                        if ( is_wp_error( $source_object ) ) {
269                                throw new Exception( $source_object->get_error_message() );
270                        }
271
272                        // SEPA Direct Debit payments do not require any customer action after the source has been created.
273                        // Once the customer has provided their IBAN details and accepted the mandate, no further action is needed and the resulting source is directly chargeable.
274                        if ( WC_Stripe_Payment_Methods::SEPA_DEBIT === $source_object->type ) {
275                                $response = [
276                                        'status' => 'success',
277                                ];
278                                echo wp_json_encode( $response );
279                                return;
280                        }
281
282                        // 4. Generate the setup intent
283                        $setup_intent = WC_Stripe_API::request(
284                                [
285                                        'customer'             => $customer->get_id(),
286                                        'confirm'              => 'true',
287                                        'payment_method'       => $source_id,
288                                        'payment_method_types' => [ $source_object->type ],
289                                ],
290                                'setup_intents'
291                        );
292
293                        if ( ! empty( $setup_intent->error ) ) {
294                                $error_response_message = print_r( $setup_intent, true );
295                                WC_Stripe_Logger::log( 'Failed create Setup Intent while saving a card.' );
296                                WC_Stripe_Logger::log( "Response: $error_response_message" );
297                                throw new Exception( __( 'Your card could not be set up for future usage.', 'woocommerce-gateway-stripe' ) );
298                        }
299
300                        // 5. Respond.
301                        if ( WC_Stripe_Intent_Status::REQUIRES_ACTION === $setup_intent->status ) {
302                                $response = [
303                                        'status'        => WC_Stripe_Intent_Status::REQUIRES_ACTION,
304                                        'client_secret' => $setup_intent->client_secret,
305                                ];
306                        } elseif ( WC_Stripe_Intent_Status::REQUIRES_PAYMENT_METHOD === $setup_intent->status
307                                || WC_Stripe_Intent_Status::REQUIRES_CONFIRMATION === $setup_intent->status
308                                || WC_Stripe_Intent_Status::CANCELED === $setup_intent->status ) {
309                                // These statuses should not be possible, as such we return an error.
310                                $response = [
311                                        'status' => 'error',
312                                        'error'  => [
313                                                'type'    => 'setup_intent_error',
314                                                'message' => __( 'Failed to save payment method.', 'woocommerce-gateway-stripe' ),
315                                        ],
316                                ];
317                        } else {
318                                // This should only be reached when status is `processing` or `succeeded`, which are
319                                // the only statuses that we haven't explicitly handled.
320                                $response = [
321                                        'status' => 'success',
322                                ];
323                        }
324                } catch ( Exception $e ) {
325                        $response = [
326                                'status' => 'error',
327                                'error'  => [
328                                        'type'    => 'setup_intent_error',
329                                        'message' => $e->getMessage(),
330                                ],
331                        ];
332                }
333
334                echo wp_json_encode( $response );
335                exit;
336        }
337
338        /**
339         * Handle AJAX requests for creating a payment intent for Stripe UPE.
340         */
341        public function create_payment_intent_ajax() {
342                try {
343                        $is_nonce_valid = check_ajax_referer( 'wc_stripe_create_payment_intent_nonce', false, false );
344                        if ( ! $is_nonce_valid ) {
345                                throw new Exception( __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-gateway-stripe' ) );
346                        }
347
348                        // If paying from order, we need to get the total from the order instead of the cart.
349                        $order_id            = isset( $_POST['stripe_order_id'] ) ? absint( $_POST['stripe_order_id'] ) : null;
350                        $payment_method_type = isset( $_POST['payment_method_type'] ) ? wc_clean( wp_unslash( $_POST['payment_method_type'] ) ) : '';
351
352                        if ( $order_id ) {
353                                $order = wc_get_order( $order_id );
354                                if ( ! $order || ! $order->needs_payment() ) {
355                                        throw new Exception( __( 'Unable to process your request. Please reload the page and try again.', 'woocommerce-gateway-stripe' ) );
356                                }
357                        }
358
359                        wp_send_json_success( $this->create_payment_intent( $order_id, $payment_method_type ), 200 );
360                } catch ( Exception $e ) {
361                        WC_Stripe_Logger::log( 'Create payment intent error: ' . $e->getMessage() );
362                        // Send back error so it can be displayed to the customer.
363                        wp_send_json_error(
364                                [
365                                        'error' => [
366                                                'message' => $e->getMessage(),
367                                        ],
368                                ]
369                        );
370                }
371        }
372
373        /**
374         * Creates payment intent using current cart or order and store details.
375         *
376         * @param int|null    $order_id The id of the order if intent created from Order.
377         * @param string|null $payment_method_type The type of payment method to use for the intent.
378         *
379         * @throws Exception - If the create intent call returns with an error.
380         * @return array
381         */
382        public function create_payment_intent( $order_id = null, $payment_method_type = null ) {
383                $amount = WC()->cart->get_total( false );
384                $order  = wc_get_order( $order_id );
385                if ( is_a( $order, 'WC_Order' ) ) {
386                        $amount = $order->get_total();
387                }
388
389                $gateway                 = $this->get_upe_gateway();
390                $enabled_payment_methods = $payment_method_type ? [ $payment_method_type ] : $gateway->get_upe_enabled_at_checkout_payment_method_ids( $order_id );
391
392                $currency = get_woocommerce_currency();
393                $capture  = $gateway->is_automatic_capture_enabled();
394                $request  = [
395                        'amount'               => WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( $currency ) ),
396                        'currency'             => strtolower( $currency ),
397                        'payment_method_types' => $enabled_payment_methods,
398                        'capture_method'       => $capture ? 'automatic' : 'manual',
399                ];
400
401                $request = $this->maybe_add_mandate_options( $request, $payment_method_type );
402
403                $payment_intent = WC_Stripe_API::request( $request, 'payment_intents' );
404
405                if ( ! empty( $payment_intent->error ) ) {
406                        throw new Exception( $payment_intent->error->message );
407                }
408
409                return [
410                        'id'            => $payment_intent->id,
411                        'client_secret' => $payment_intent->client_secret,
412                ];
413        }
414
415        /**
416         * Handle AJAX request for updating a payment intent for Stripe UPE.
417         *
418         * @since 5.6.0
419         */
420        public function update_payment_intent_ajax() {
421                try {
422                        $is_nonce_valid = check_ajax_referer( 'wc_stripe_update_payment_intent_nonce', false, false );
423                        if ( ! $is_nonce_valid ) {
424                                throw new Exception( __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-gateway-stripe' ) );
425                        }
426
427                        $order_id                  = isset( $_POST['stripe_order_id'] ) ? absint( $_POST['stripe_order_id'] ) : null;
428                        $payment_intent_id         = isset( $_POST['wc_payment_intent_id'] ) ? wc_clean( wp_unslash( $_POST['wc_payment_intent_id'] ) ) : '';
429                        $save_payment_method       = isset( $_POST['save_payment_method'] ) ? 'yes' === wc_clean( wp_unslash( $_POST['save_payment_method'] ) ) : false;
430                        $selected_upe_payment_type = ! empty( $_POST['selected_upe_payment_type'] ) ? wc_clean( wp_unslash( $_POST['selected_upe_payment_type'] ) ) : '';
431
432                        $order_from_payment = WC_Stripe_Helper::get_order_by_intent_id( $payment_intent_id );
433                        if ( ! $order_from_payment || $order_from_payment->get_id() !== $order_id ) {
434                                throw new Exception( __( 'Unable to verify your request. Please reload the page and try again.', 'woocommerce-gateway-stripe' ) );
435                        }
436
437                        $update_intent_result = $this->update_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_upe_payment_type );
438
439                        if ( ! ( $update_intent_result['success'] ?? false ) ) {
440                                $error_message = $update_intent_result['error'] ?? __( "We're not able to process this request. Please try again later.", 'woocommerce-gateway-stripe' );
441                                wp_send_json_error(
442                                        [
443                                                'error' => [
444                                                        'message' => $error_message,
445                                                ],
446                                        ]
447                                );
448                        } else {
449                                wp_send_json_success( $update_intent_result, 200 );
450                        }
451                } catch ( Exception $e ) {
452                        // Send back error so it can be displayed to the customer.
453                        wp_send_json_error(
454                                [
455                                        'error' => [
456                                                'message' => $e->getMessage(),
457                                        ],
458                                ]
459                        );
460                }
461        }
462
463        /**
464         * Updates payment intent or setup intent to be able to save payment method.
465         *
466         * @since 5.6.0
467         * @version 9.4.0
468         *
469         * @param string  $intent_id                 The id of the payment intent or setup intent to update.
470         * @param int     $order_id                  The id of the order if intent created from Order.
471         * @param boolean $save_payment_method       True if saving the payment method.
472         * @param string  $selected_upe_payment_type The name of the selected UPE payment type or empty string.
473         *
474         * @throws Exception  If the update intent call returns with an error.
475         * @return array|null An array with result of the update, or nothing
476         */
477        public function update_intent( $intent_id = '', $order_id = null, $save_payment_method = false, $selected_upe_payment_type = '' ) {
478                $order = wc_get_order( $order_id );
479
480                if ( ! is_a( $order, 'WC_Order' ) ) {
481                        return [
482                                'success' => false,
483                                'error'   => __( 'Unable to find a matching order.', 'woocommerce-gateway-stripe' ),
484                        ];
485                }
486
487                $order_helper = WC_Stripe_Order_Helper::get_instance();
488
489                $selected_payment_type = '' !== $selected_upe_payment_type && is_string( $selected_upe_payment_type ) ? $selected_upe_payment_type : null;
490                $order_helper->validate_intent_for_order( $order, $intent_id, $selected_payment_type );
491
492                $gateway  = $this->get_upe_gateway();
493                $amount   = $order->get_total();
494                $currency = $order->get_currency();
495                $customer = new WC_Stripe_Customer( wp_get_current_user()->ID );
496                $customer->maybe_create_customer();
497
498                if ( $intent_id ) {
499                        $request = [
500                                'metadata'    => $gateway->get_metadata_from_order( $order ),
501                                /* translators: 1) blog name 2) order number */
502                                'description' => sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() ),
503                        ];
504
505                        $is_setup_intent = substr( $intent_id, 0, 4 ) === 'seti';
506                        if ( ! $is_setup_intent ) {
507                                // These parameters are only supported for payment intents.
508                                $request['amount']   = WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( $currency ) );
509                                $request['currency'] = strtolower( $currency );
510                        }
511
512                        if ( '' !== $selected_upe_payment_type ) {
513                                // Only update the payment_method_types if we have a reference to the payment type the customer selected.
514                                $request['payment_method_types'] = [ $selected_upe_payment_type ];
515                                if (
516                                        WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID === $selected_upe_payment_type &&
517                                        in_array(
518                                                WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID,
519                                                $gateway->get_upe_enabled_payment_method_ids(),
520                                                true
521                                        )
522                                ) {
523                                        $request['payment_method_types'] = [
524                                                WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID,
525                                                WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID,
526                                        ];
527                                }
528                                $order_helper->update_stripe_upe_payment_type( $order, $selected_upe_payment_type );
529                        }
530                        if ( ! empty( $customer ) && $customer->get_id() ) {
531                                $request['customer'] = $customer->get_id();
532                        }
533                        if ( $save_payment_method ) {
534                                $request['setup_future_usage'] = 'off_session';
535                        }
536
537                        $level3_data = $gateway->get_level3_data_from_order( $order );
538
539                        // Use "setup_intents" endpoint if `$intent_id` starts with `seti_`.
540                        $endpoint = $is_setup_intent ? 'setup_intents' : 'payment_intents';
541                        $result = WC_Stripe_API::request_with_level3_data(
542                                $request,
543                                "{$endpoint}/{$intent_id}",
544                                $level3_data,
545                                $order
546                        );
547
548                        if ( ! empty( $result->error ) ) {
549                                if ( 'payment_intent_unexpected_state' === $result->error->code ) {
550                                        WC_Stripe_Logger::critical(
551                                                'Error: Failed to update intent due to invalid operation',
552                                                [
553                                                        'intent_id'   => $intent_id,
554                                                        'order_id'    => $order_id,
555                                                        'error'       => $result->error,
556                                                ]
557                                        );
558
559                                        throw new Exception( __( "We're not able to process this request. Please try again later.", 'woocommerce-gateway-stripe' ) );
560                                }
561
562                                WC_Stripe_Logger::error(
563                                        'Error: Failed to update Stripe intent',
564                                        [
565                                                'intent_id' => $intent_id,
566                                                'order_id'  => $order_id,
567                                                'error'     => $result->error,
568                                        ]
569                                );
570
571                                return [
572                                        'success' => false,
573                                        'error'   => $result->error->message,
574                                ];
575                        }
576
577                        // Prevent any failures if updating the status of a subscription order.
578                        if ( ! $gateway->has_subscription( $order_id ) ) {
579                                $order->update_status( OrderStatus::PENDING, __( 'Awaiting payment.', 'woocommerce-gateway-stripe' ) );
580                        }
581                        $order->save();
582                        $order_helper->add_payment_intent_to_order( $intent_id, $order );
583                }
584
585                return [
586                        'success' => true,
587                ];
588        }
589
590        /**
591         * Handle AJAX requests for creating a setup intent without confirmation for Stripe UPE.
592         *
593         * @since 5.6.0
594         * @version 9.4.0
595         */
596        public function init_setup_intent_ajax() {
597                try {
598                        $is_nonce_valid = check_ajax_referer( 'wc_stripe_create_setup_intent_nonce', false, false );
599                        if ( ! $is_nonce_valid ) {
600                                throw new Exception( __( "We're not able to add this payment method. Please refresh the page and try again.", 'woocommerce-gateway-stripe' ) );
601                        }
602
603                        $payment_method_type = isset( $_POST['payment_method_type'] ) ? wc_clean( wp_unslash( $_POST['payment_method_type'] ) ) : '';
604
605                        wp_send_json_success( $this->init_setup_intent( $payment_method_type ), 200 );
606                } catch ( Exception $e ) {
607                        // Send back error, so it can be displayed to the customer.
608                        wp_send_json_error(
609                                [
610                                        'error' => [
611                                                'message' => $e->getMessage(),
612                                        ],
613                                ]
614                        );
615                }
616        }
617
618        /**
619         * Creates a setup intent without confirmation.
620         *
621         * @since 5.6.0
622         * @version 9.4.0
623         *
624         * @param string|null $payment_method_type The type of payment method to use for the intent.
625         * @return array
626         * @throws Exception If customer for the current user cannot be read/found.
627         */
628        public function init_setup_intent( $payment_method_type = null ) {
629                // Determine the customer managing the payment methods, create one if we don't have one already.
630                $user     = wp_get_current_user();
631                $customer = new WC_Stripe_Customer( $user->ID );
632
633                if ( ! $customer->get_id() ) {
634                        $customer_id = $customer->create_customer();
635                } else {
636                        $customer_id = $customer->update_customer();
637                }
638
639                $gateway                 = $this->get_upe_gateway();
640                $enabled_payment_methods = $payment_method_type ? [ $payment_method_type ] : array_values( array_filter( $gateway->get_upe_enabled_payment_method_ids(), [ $gateway, 'is_enabled_for_saved_payments' ] ) );
641
642                $request = [
643                        'customer'             => $customer_id,
644                        'confirm'              => 'false',
645                        'payment_method_types' => $enabled_payment_methods,
646                ];
647
648                $request = $this->maybe_add_mandate_options( $request, $payment_method_type, true );
649
650                $setup_intent = WC_Stripe_API::request( $request, 'setup_intents' );
651
652                if ( ! empty( $setup_intent->error ) ) {
653                        throw new Exception( $setup_intent->error->message );
654                }
655
656                return [
657                        'id'            => $setup_intent->id,
658                        'client_secret' => $setup_intent->client_secret,
659                ];
660        }
661
662        /**
663         * Handle AJAX request after authenticating payment at checkout.
664         *
665         * This function is used to update the order status after the user has
666         * been asked to authenticate their payment.
667         *
668         * This function is used for both:
669         * - regular checkout
670         * - Pay for Order page (in theory).
671         *
672         * @throws WC_Stripe_Exception
673         */
674        public function update_order_status_ajax() {
675                $order_helper = WC_Stripe_Order_Helper::get_instance();
676                $order        = false;
677
678                try {
679                        $is_nonce_valid = check_ajax_referer( 'wc_stripe_update_order_status_nonce', false, false );
680                        if ( ! $is_nonce_valid ) {
681                                throw new WC_Stripe_Exception( 'missing-nonce', __( 'CSRF verification failed.', 'woocommerce-gateway-stripe' ) );
682                        }
683
684                        $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : false;
685                        $order    = wc_get_order( $order_id );
686                        if ( ! $order ) {
687                                throw new WC_Stripe_Exception( 'order_not_found', __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
688                        }
689
690                        $intent_id          = $order_helper->get_intent_id_from_order( $order );
691                        $intent_id_received = isset( $_POST['intent_id'] ) ? wc_clean( wp_unslash( $_POST['intent_id'] ) ) : null;
692                        if ( empty( $intent_id_received ) || $intent_id_received !== $intent_id ) {
693                                $note = sprintf(
694                                        /* translators: %1: transaction ID of the payment or a translated string indicating an unknown ID. */
695                                        __( 'A payment with ID %s was used in an attempt to pay for this order. This payment intent ID does not match any payments for this order, so it was ignored and the order was not updated.', 'woocommerce-gateway-stripe' ),
696                                        $intent_id_received
697                                );
698                                $order->add_order_note( $note );
699                                throw new WC_Stripe_Exception( 'invalid_intent_id', __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
700                        }
701                        $save_payment_method = isset( $_POST['payment_method_id'] ) && ! empty( wc_clean( wp_unslash( $_POST['payment_method_id'] ) ) );
702
703                        $gateway = $this->get_upe_gateway();
704                        $gateway->process_order_for_confirmed_intent( $order, $intent_id_received, $save_payment_method );
705                        wp_send_json_success(
706                                [
707                                        'return_url' => $gateway->get_return_url( $order ),
708                                ],
709                                200
710                        );
711                } catch ( WC_Stripe_Exception $e ) {
712                        wc_add_notice( $e->getLocalizedMessage(), 'error' );
713                        WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
714
715                        /* translators: error message */
716                        if ( $order ) {
717                                // Remove the awaiting confirmation order meta, don't save the order since it'll be saved in the next `update_status()` call.
718                                $order_helper->remove_payment_awaiting_action( $order, false );
719                                $order->update_status( OrderStatus::FAILED );
720                        }
721
722                        // Send back error so it can be displayed to the customer.
723                        wp_send_json_error(
724                                [
725                                        'error' => [
726                                                'message' => $e->getLocalizedMessage(),
727                                        ],
728                                ]
729                        );
730                }
731        }
732
733        /**
734         * Handle AJAX request if error occurs while confirming intent.
735         * We will log the error and update the order.
736         *
737         * @throws WC_Stripe_Exception
738         */
739        public function update_failed_order_ajax() {
740                $order = false;
741                try {
742                        $is_nonce_valid = check_ajax_referer( 'wc_stripe_update_failed_order_nonce', false, false );
743                        if ( ! $is_nonce_valid ) {
744                                throw new WC_Stripe_Exception( 'missing-nonce', __( 'CSRF verification failed.', 'woocommerce-gateway-stripe' ) );
745                        }
746
747                        $order_id  = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : null;
748                        $intent_id = isset( $_POST['intent_id'] ) ? wc_clean( wp_unslash( $_POST['intent_id'] ) ) : '';
749                        $order     = wc_get_order( $order_id );
750
751                        $order_from_payment = WC_Stripe_Helper::get_order_by_intent_id( $intent_id );
752                        if ( ! $order_from_payment || $order_from_payment->get_id() !== $order_id ) {
753                                wp_send_json_error( __( 'Unable to verify your request. Please reload the page and try again.', 'woocommerce-gateway-stripe' ) );
754                        }
755
756                        if ( ! empty( $order_id ) && ! empty( $intent_id ) && is_object( $order ) ) {
757                                $payment_needed = 0 < $order->get_total();
758                                if ( $payment_needed ) {
759                                        $intent = WC_Stripe_API::retrieve( "payment_intents/$intent_id" );
760                                } else {
761                                        $intent = WC_Stripe_API::retrieve( "setup_intents/$intent_id" );
762                                }
763                                $error = $intent->last_payment_error || $intent->error;
764
765                                if ( ! empty( $error ) ) {
766                                        WC_Stripe_Logger::log( 'Error when processing payment: ' . $error->message );
767                                        throw new WC_Stripe_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
768                                }
769
770                                // Use the last charge within the intent to proceed.
771                                $gateway = $this->get_gateway();
772                                $charge  = $gateway->get_latest_charge_from_intent( $intent );
773                                if ( ! empty( $charge ) ) {
774                                        $gateway->process_response( $charge, $order );
775                                } else {
776                                        // TODO: Add implementation for setup intents.
777                                        $gateway->process_response( $intent, $order );
778                                }
779                                $gateway->save_intent_to_order( $order, $intent );
780                        }
781                } catch ( WC_Stripe_Exception $e ) {
782                        // We are expecting an exception to be thrown here.
783                        wc_add_notice( $e->getLocalizedMessage(), 'error' );
784                        WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
785
786                        do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
787
788                        if ( $order ) {
789                                $order->update_status( OrderStatus::FAILED );
790                        }
791                }
792
793                wp_send_json_success();
794        }
795
796        /**
797         * Creates and confirm a payment intent with the given payment information.
798         * Used for dPE.
799         *
800         * @param array $payment_information The payment information needed for creating and confirming the intent.
801         *
802         * @throws WC_Stripe_Exception - If the create intent call returns with an error.
803         *
804         * @return stdClass
805         */
806        public function create_and_confirm_payment_intent( $payment_information ) {
807                // Throws a WC_Stripe_Exception if required information is missing.
808                $required_params = [
809                        'amount',
810                        'currency',
811                        'customer',
812                        'level3',
813                        'metadata',
814                        'order',
815                        'save_payment_method_to_store',
816                        'shipping',
817                ];
818
819                $non_empty_params = [];
820
821                // The payment method is not required if we're using the confirmation token flow.
822                if ( empty( $payment_information['confirmation_token'] ) ) {
823                        $required_params[] = 'payment_method';
824                        $required_params[] = 'capture_method';
825
826                        $non_empty_params[] = 'payment_method';
827                }
828
829                $instance_params = [ 'order' => 'WC_Order' ];
830
831                $this->validate_payment_intent_required_params( $required_params, $non_empty_params, $instance_params, $payment_information );
832
833                $order                 = $payment_information['order'];
834                $selected_payment_type = $payment_information['selected_payment_type'];
835                $payment_method_types  = $payment_information['payment_method_types'];
836                $is_using_saved_token  = $payment_information['is_using_saved_payment_method'] ?? false;
837
838                $request = $this->build_base_payment_intent_request_params( $payment_information );
839
840                $request = array_merge(
841                        $request,
842                        [
843                                'amount'               => $payment_information['amount'],
844                                'confirm'              => 'true',
845                                'currency'             => $payment_information['currency'],
846                                'customer'             => $payment_information['customer'],
847                                /* translators: 1) blog name 2) order number */
848                                'description'          => sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() ),
849                                'metadata'             => $payment_information['metadata'],
850                                'payment_method_types' => $payment_method_types,
851                        ]
852                );
853
854                if ( isset( $payment_information['statement_descriptor_suffix'] ) ) {
855                        $request['statement_descriptor_suffix'] = $payment_information['statement_descriptor_suffix'];
856                }
857
858                if ( ! empty( $payment_information['payment_method_options'] ) ) {
859                        $request['payment_method_options'] = $payment_information['payment_method_options'];
860                }
861
862                // Using a saved token will also be confirmed immediately. For voucher and wallet payment methods type like Boleto, Oxxo, Multibanco, and Cash App we shouldn't confirm
863                // the intent immediately as this is done on the front-end when displaying the voucher to the customer.
864                // When the intent is confirmed, Stripe sends a webhook to the store which puts the order on-hold, which we only want to happen after successfully displaying the voucher.
865                if ( ! $is_using_saved_token && $this->is_delayed_confirmation_required( $payment_method_types ) ) {
866                        $request['confirm'] = 'false';
867
868                        // When `confirm` is `false`, `return_url` and `mandate_data` are not accepted
869                        unset( $request['return_url'], $request['mandate_data'] );
870                }
871
872                // Run the necessary filter to make sure mandate information is added when it's required.
873                $request = apply_filters(
874                        'wc_stripe_generate_create_intent_request',
875                        $request,
876                        $order,
877                        null // $prepared_source parameter is not necessary for adding mandate information.
878                );
879
880                $payment_intent = WC_Stripe_API::request_with_level3_data(
881                        $request,
882                        'payment_intents',
883                        $payment_information['level3'],
884                        $order
885                );
886
887                // Only update the payment_type if we have a reference to the payment type the customer selected.
888                if ( '' !== $selected_payment_type ) {
889                        WC_Stripe_Order_Helper::get_instance()->update_stripe_upe_payment_type( $order, $selected_payment_type );
890                }
891
892                return $payment_intent;
893        }
894
895        /**
896         * Adds mandate options to the request if required.
897         *
898         * @param array            $request              The request array to add the mandate options to.
899         * @param string|null      $payment_method_type  The type of payment method to use for the intent.
900         * @param bool             $is_setup_intent      Whether the request is for a setup intent.
901         * @param WC_Order|null    $order                The order object.
902         *
903         * @return array
904         */
905        private function maybe_add_mandate_options( $request, $payment_method_type, $is_setup_intent = false, $order = null ) {
906                if ( WC_Stripe_UPE_Payment_Method_ACSS::STRIPE_ID === $payment_method_type ) {
907                        $request['payment_method_options'] = [
908                                WC_Stripe_Payment_Methods::ACSS_DEBIT => [
909                                        'mandate_options' => [
910                                                'payment_schedule'     => 'combined',
911                                                'interval_description' => __( 'Payments as per agreement', 'woocommerce-gateway-stripe' ),
912                                                'transaction_type'     => 'personal',
913                                        ],
914                                ],
915                        ];
916
917                        // If it's a setup intent, add the CAD currency parameter.
918                        if ( $is_setup_intent ) {
919                                $request['payment_method_options'][ WC_Stripe_Payment_Methods::ACSS_DEBIT ]['currency'] = strtolower( WC_Stripe_Currency_Code::CANADIAN_DOLLAR );
920                        }
921                }
922
923                if ( WC_Stripe_Payment_Methods::CARD === $payment_method_type && $order && $is_setup_intent ) {
924                        // Run the necessary filter to make sure correct mandate information is added for recurring card payments for subscriptions.
925                        $request = apply_filters(
926                                'wc_stripe_generate_create_intent_request',
927                                $request,
928                                $order,
929                                null, // $prepared_source parameter is not necessary for adding mandate information.
930                                true // $is_setup_intent parameter is true for setup intents.
931                        );
932                }
933
934                return $request;
935        }
936
937        /**
938         * Updates and confirm a payment intent with the given payment information.
939         * Used for dPE.
940         *
941         * @param object $payment_intent       The payment intent to update.
942         * @param array $payment_information The payment information needed for creating and confirming the intent.
943         *
944         * @throws WC_Stripe_Exception - If any of the required information is missing.
945         *
946         * @return array
947         */
948        public function update_and_confirm_payment_intent( $payment_intent, $payment_information ) {
949                // Throws a WC_Stripe_Exception if required information is missing.
950                $required_params = [
951                        'shipping',
952                        'selected_payment_type',
953                        'payment_method_types',
954                        'level3',
955                        'order',
956                        'save_payment_method_to_store',
957                ];
958
959                // The payment method is not required if we're using the confirmation token flow.
960                if ( empty( $payment_information['confirmation_token'] ) ) {
961                        $required_params[] = 'payment_method';
962                        $required_params[] = 'capture_method';
963                }
964
965                $instance_params = [ 'order' => 'WC_Order' ];
966
967                $this->validate_payment_intent_required_params( $required_params, [], $instance_params, $payment_information );
968
969                $request = $this->build_base_payment_intent_request_params( $payment_information );
970
971                $order = $payment_information['order'];
972
973                // Run the necessary filter to make sure mandate information is added when it's required.
974                $request = apply_filters(
975                        'wc_stripe_generate_create_intent_request',
976                        $request,
977                        $order,
978                        null // $prepared_source parameter is not necessary for adding mandate information.
979                );
980
981                return WC_Stripe_API::request_with_level3_data(
982                        $request,
983                        "payment_intents/{$payment_intent->id}/confirm",
984                        $payment_information['level3'],
985                        $order
986                );
987        }
988
989        /**
990         * Determines if the request contains all the required params for creating or updating a payment intent.
991         *
992         * @param array $required_params The required parameters for the payment intent.
993         * @param array $non_empty_params The parameters that must not contain an empty value.
994         * @param array $instance_params The parameters that must be of a specific type.
995         * @param array $payment_information The payment information to be validated.
996         * @return void
997         * @throws WC_Stripe_Exception
998         */
999        private function validate_payment_intent_required_params( $required_params, $non_empty_params, $instance_params, $payment_information ) {
1000                $missing_params = [];
1001                foreach ( $required_params as $param ) {
1002                        // Check if they're set. Some can be null.
1003                        if ( ! array_key_exists( $param, $payment_information ) ) {
1004                                $missing_params[] = $param;
1005                        }
1006                }
1007
1008                // Some params must not contain an empty value.
1009                foreach ( $non_empty_params as $param ) {
1010                        if ( empty( $payment_information[ $param ] ) ) {
1011                                $missing_params[] = $param;
1012                        }
1013                }
1014
1015                $shopper_error_message = __( 'There was a problem processing the payment.', 'woocommerce-gateway-stripe' );
1016
1017                // Bail out if we're missing required information.
1018                if ( ! empty( $missing_params ) ) {
1019                        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
1020                        $calling_method = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 )[1]['function'] ?? '';
1021                        throw new WC_Stripe_Exception(
1022                                sprintf(
1023                                        'The information for creating and confirming the intent is missing the following data: %s. Payment information received: %s. Calling method: %s',
1024                                        implode( ', ', $missing_params ),
1025                                        wp_json_encode( $payment_information ),
1026                                        $calling_method
1027                                ),
1028                                $shopper_error_message
1029                        );
1030                }
1031
1032                // Check if the instance params are of the correct type.
1033                foreach ( $instance_params as $param => $type ) {
1034                        if ( ! is_a( $payment_information[ $param ], $type ) ) {
1035                                throw new WC_Stripe_Exception(
1036                                        sprintf(
1037                                                'The provided value for the "%s" parameter is not a %s.',
1038                                                $param,
1039                                                $type
1040                                        ),
1041                                        __( 'Please reach out to us if the problem persists.', 'woocommerce-gateway-stripe' )
1042                                );
1043                        }
1044                }
1045        }
1046
1047        /**
1048         * Builds the base request parameters for creating/updating and confirming a payment intent.
1049         *
1050         * @param array $payment_information The payment information needed for creating/updating and confirming the intent.
1051         *
1052         * @return array The request parameters for creating/updating and confirming a payment intent.
1053         */
1054        private function build_base_payment_intent_request_params( $payment_information ) {
1055                $selected_payment_type = $payment_information['selected_payment_type'];
1056                if ( $this->get_upe_gateway()->is_oc_enabled() && isset( $payment_information['payment_method_details']->type ) ) {
1057                        $selected_payment_type = $payment_information['payment_method_details']->type;
1058                }
1059
1060                $payment_method_types = $payment_information['payment_method_types'];
1061
1062                $request = [
1063                        'shipping' => $payment_information['shipping'],
1064                ];
1065
1066                $is_using_confirmation_token = ! empty( $payment_information['confirmation_token'] );
1067                if ( $is_using_confirmation_token ) {
1068                        $request['confirmation_token'] = $payment_information['confirmation_token'];
1069                } else {
1070                        $request['payment_method'] = $payment_information['payment_method'];
1071                        $request['capture_method'] = $payment_information['capture_method'];
1072
1073                }
1074
1075                // For Stripe Link & SEPA with deferred intent UPE, we must create mandate to acknowledge that terms have been shown to customer.
1076                if ( ! $is_using_confirmation_token && $this->is_mandate_data_required( $selected_payment_type ) ) {
1077                        $request = WC_Stripe_Helper::add_mandate_data( $request );
1078                }
1079
1080                $request = $this->maybe_add_mandate_options( $request, $payment_information['selected_payment_type'] );
1081
1082                // Does not set the return URL if the request needs redirection.
1083                if ( $this->request_needs_redirection( $payment_method_types ) ) {
1084                        $request['return_url'] = $payment_information['return_url'];
1085                }
1086
1087                // If the customer is saving the payment method to the store or has a subscription, we should set the setup_future_usage to off_session.
1088                // Only exceptions are when using a confirmation token or manual renewal is required.
1089                // For confirmations tokens, the setup_future_usage is set within the payment method.
1090                $payment_method                 = WC_Stripe_UPE_Payment_Gateway::get_payment_method_instance( $selected_payment_type );
1091                $has_auto_renewing_subscription = ! empty( $payment_information['has_subscription'] ) && ! $this->is_manual_renewal_required( $payment_method->is_reusable() );
1092                if ( ! $is_using_confirmation_token && ( $payment_information['save_payment_method_to_store'] || $has_auto_renewing_subscription ) ) {
1093                        $request['setup_future_usage'] = 'off_session';
1094                }
1095
1096                // BLIK requires additional information in the payment method options.
1097                if ( WC_Stripe_Payment_Methods::BLIK === $selected_payment_type && isset( $payment_information['payment_method_options'] ) ) {
1098                        $request['payment_method_options'] = $payment_information['payment_method_options'];
1099                }
1100
1101                return $request;
1102        }
1103
1104        /**
1105         * Determines if mandate data is required for deferred intent UPE payment.
1106         *
1107         * A mandate must be provided before a deferred intent UPE payment can be processed.
1108         * This applies to SEPA, Bancontact, iDeal, Sofort, Cash App, Link payment methods,
1109         * ACH, ACSS Debit and BACS.
1110         * https://docs.stripe.com/payments/finalize-payments-on-the-server
1111         *
1112         * @param string $selected_payment_type         The name of the selected UPE payment type.
1113         * @param bool   $is_using_saved_payment_method Option. True if the customer is using a saved payment method, false otherwise.
1114         *
1115         * @return bool True if a mandate must be shown and acknowledged by customer before deferred intent UPE payment can be processed, false otherwise.
1116         */
1117        public function is_mandate_data_required( $selected_payment_type, $is_using_saved_payment_method = false ) {
1118                $payment_methods_with_mandates = [
1119                        WC_Stripe_Payment_Methods::ACH,
1120                        WC_Stripe_Payment_Methods::ACSS_DEBIT,
1121                        WC_Stripe_Payment_Methods::AMAZON_PAY,
1122                        WC_Stripe_Payment_Methods::BACS_DEBIT,
1123                        WC_Stripe_Payment_Methods::BECS_DEBIT,
1124                        WC_Stripe_Payment_Methods::SEPA_DEBIT,
1125                        WC_Stripe_Payment_Methods::BANCONTACT,
1126                        WC_Stripe_Payment_Methods::IDEAL,
1127                        WC_Stripe_Payment_Methods::SOFORT,
1128                        WC_Stripe_Payment_Methods::LINK,
1129                ];
1130                if ( in_array( $selected_payment_type, $payment_methods_with_mandates, true ) ) {
1131                        return true;
1132                }
1133
1134                return WC_Stripe_Payment_Methods::CARD === $selected_payment_type && in_array( WC_Stripe_Payment_Methods::LINK, $this->get_upe_gateway()->get_upe_enabled_payment_method_ids(), true );
1135        }
1136
1137        /**
1138         * Creates and confirm a setup intent with the given payment method ID.
1139         *
1140         * @param array $payment_information The payment information to be used for the setup intent.
1141         *
1142         * @throws WC_Stripe_Exception If the create intent call returns with an error.
1143         *
1144         * @return stdClass
1145         */
1146        public function create_and_confirm_setup_intent( $payment_information ) {
1147                $request = [
1148                        'payment_method'       => $payment_information['payment_method'],
1149                        'payment_method_types' => $payment_information['payment_method_types'] ?? [ $payment_information['selected_payment_type'] ],
1150                        'customer'             => $payment_information['customer'],
1151                        'confirm'              => 'true',
1152                        'return_url'           => $payment_information['return_url'],
1153                ];
1154
1155                if ( isset( $payment_information['use_stripe_sdk'] ) ) {
1156                        $request['use_stripe_sdk'] = $payment_information['use_stripe_sdk'];
1157                }
1158
1159                // SEPA setup intents require mandate data.
1160                if ( $this->is_mandate_data_required( $payment_information['selected_payment_type'] ) ) {
1161                        $request = WC_Stripe_Helper::add_mandate_data( $request );
1162                }
1163
1164                $request = $this->maybe_add_mandate_options( $request, $payment_information['selected_payment_type'], true, $payment_information['order'] ?? null );
1165
1166                // For voucher payment methods type like Boleto, Oxxo, Multibanco, and Cash App, we shouldn't confirm the intent immediately as this is done on the front-end when displaying the voucher to the customer.
1167                // When the intent is confirmed, Stripe sends a webhook to the store which puts the order on-hold, which we only want to happen after successfully displaying the voucher.
1168                if ( $this->is_delayed_confirmation_required( $request['payment_method_types'] ) ) {
1169                        $request['confirm'] = 'false';
1170                }
1171
1172                // Removes the return URL if the request doesn't need redirection.
1173                if ( ! $this->request_needs_redirection( $request['payment_method_types'] ) ) {
1174                        unset( $request['return_url'] );
1175                }
1176
1177                $setup_intent = WC_Stripe_API::request( $request, 'setup_intents' );
1178
1179                if ( ! empty( $setup_intent->error ) ) {
1180                        throw new WC_Stripe_Exception( print_r( $setup_intent->error, true ), $setup_intent->error->message );
1181                }
1182
1183                return $setup_intent;
1184        }
1185
1186        /**
1187         * Handle AJAX requests for creating and confirming a setup intent.
1188         *
1189         * @throws Exception If the AJAX request is missing the required data or if there's an error creating and confirming the setup intent.
1190         */
1191        public function create_and_confirm_setup_intent_ajax() {
1192                $wc_add_payment_method_rate_limit_id = 'add_payment_method_' . get_current_user_id();
1193
1194                try {
1195                        // similar rate limiter is present in WC Core, but it's executed on page submission (and not on AJAX calls).
1196                        if ( WC_Rate_Limiter::retried_too_soon( $wc_add_payment_method_rate_limit_id ) ) {
1197                                throw new WC_Stripe_Exception( 'Failed to save payment method.', __( 'You cannot add a new payment method so soon after the previous one.', 'woocommerce-gateway-stripe' ) );
1198                        }
1199
1200                        $is_nonce_valid = check_ajax_referer( 'wc_stripe_create_and_confirm_setup_intent_nonce', false, false );
1201                        if ( ! $is_nonce_valid ) {
1202                                throw new WC_Stripe_Exception( 'Invalid nonce.', __( 'Unable to verify your request. Please refresh the page and try again.', 'woocommerce-gateway-stripe' ) );
1203                        }
1204
1205                        /**
1206                         * Filter to validate captcha for create and confirm setup intent requests.
1207                         * Can be used by third-party plugins to add captcha validation.
1208                         *
1209                         * @since 10.1.0
1210                         * @param bool $is_captcha_valid True if the captcha is valid, false otherwise. Default is true.
1211                         */
1212                        $is_captcha_valid = apply_filters( 'wc_stripe_is_valid_create_and_confirm_setup_intent_captcha', true );
1213                        if ( ! $is_captcha_valid ) {
1214                                throw new WC_Stripe_Exception( 'captcha_invalid', __( 'Captcha verification failed. Please try again.', 'woocommerce-gateway-stripe' ) );
1215                        }
1216
1217                        $payment_method = sanitize_text_field( wp_unslash( $_POST['wc-stripe-payment-method'] ?? '' ) );
1218                        $payment_type   = sanitize_text_field( wp_unslash( $_POST['wc-stripe-payment-type'] ?? WC_Stripe_Payment_Methods::CARD ) );
1219                        if ( ! $payment_method ) {
1220                                throw new WC_Stripe_Exception( 'Payment method missing from request.', __( "We're not able to add this payment method. Please refresh the page and try again.", 'woocommerce-gateway-stripe' ) );
1221                        }
1222
1223                        // Determine the customer managing the payment methods, create one if we don't have one already.
1224                        $user = wp_get_current_user();
1225                        // This page is only accessible to logged in users.
1226                        if ( ! $user->ID ) {
1227                                throw new WC_Stripe_Exception( 'User not found.', __( "We're not able to add this payment method. Please refresh the page and try again.", 'woocommerce-gateway-stripe' ) );
1228                        }
1229                        $customer = new WC_Stripe_Customer( $user->ID );
1230
1231                        // Manually create the payment information array to create & confirm the setup intent.
1232                        $payment_information = [
1233                                'payment_method'        => $payment_method,
1234                                'customer'              => $customer->update_or_create_customer( [], WC_Stripe_Customer::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD ),
1235                                'selected_payment_type' => $payment_type,
1236                                'return_url'            => wc_get_account_endpoint_url( 'payment-methods' ),
1237                                'use_stripe_sdk'        => 'true', // We want the user to complete the next steps via the JS elements. ref https://docs.stripe.com/api/setup_intents/create#create_setup_intent-use_stripe_sdk
1238                        ];
1239
1240                        // If the user has requested to update all their subscription payment methods, add a query arg to the return URL so we can handle that request upon return.
1241                        if ( ! empty( $_POST['update_all_subscription_payment_methods'] ) ) {
1242                                $payment_information['return_url'] = add_query_arg( "wc-stripe-{$payment_type}-update-all-subscription-payment-methods", 'true', $payment_information['return_url'] );
1243                        }
1244
1245                        $setup_intent = $this->create_and_confirm_setup_intent( $payment_information );
1246
1247                        if ( empty( $setup_intent->status ) || ! in_array( $setup_intent->status, WC_Stripe_Intent_Status::SUCCESSFUL_SETUP_INTENT_STATUSES, true ) ) {
1248                                throw new WC_Stripe_Exception( 'Response from Stripe: ' . print_r( $setup_intent, true ), __( 'There was an error adding this payment method. Please refresh the page and try again', 'woocommerce-gateway-stripe' ) );
1249                        }
1250
1251                        wp_send_json_success(
1252                                [
1253                                        'status'        => $setup_intent->status,
1254                                        'id'            => $setup_intent->id,
1255                                        'client_secret' => $setup_intent->client_secret,
1256                                        'next_action'   => $setup_intent->next_action,
1257                                        'payment_type'  => $payment_type,
1258                                        'return_url'    => rawurlencode( $payment_information['return_url'] ),
1259                                ],
1260                                200
1261                        );
1262                } catch ( WC_Stripe_Exception $e ) {
1263                        WC_Stripe_Logger::log( 'Failed to create and confirm setup intent. ' . $e->getMessage() );
1264
1265                        /**
1266                         * Filter the rate limit delay after a failure adding a payment method.
1267                         *
1268                         * @since 9.7.0
1269                         *
1270                         * @param int $rate_limit_delay The rate limit delay in seconds.
1271                         * @param WC_Stripe_Exception $e The exception that occurred.
1272                         */
1273                        $rate_limit_delay = apply_filters( 'wc_stripe_add_payment_method_on_error_rate_limit_delay', 10, $e );
1274
1275                        WC_Rate_Limiter::set_rate_limit( $wc_add_payment_method_rate_limit_id, $rate_limit_delay );
1276
1277                        // Send back error so it can be displayed to the customer.
1278                        wp_send_json_error(
1279                                [
1280                                        'error' => [
1281                                                'message' => $e->getLocalizedMessage(),
1282                                        ],
1283                                ]
1284                        );
1285                }
1286        }
1287
1288        /**
1289         * Confirms the change payment method request for a subscription.
1290         *
1291         * This creates the payment method token from the setup intent.
1292         *
1293         * This function is used to confirm the change payment method request for a subscription after the user has been asked to authenticate their payment (eg 3D-Secure).
1294         * It is initiated from the subscription change payment method page.
1295         */
1296        public function confirm_change_payment_from_setup_intent_ajax() {
1297                try {
1298                        $is_nonce_valid = check_ajax_referer( 'wc_stripe_update_order_status_nonce', false, false );
1299
1300                        if ( ! $is_nonce_valid ) {
1301                                throw new WC_Stripe_Exception( 'missing-nonce', __( 'CSRF verification failed.', 'woocommerce-gateway-stripe' ) );
1302                        }
1303
1304                        if ( ! function_exists( 'wcs_is_subscription' ) || ! class_exists( 'WC_Subscriptions_Change_Payment_Gateway' ) ) {
1305                                throw new WC_Stripe_Exception( 'subscriptions_not_found', __( "We're not able to process this subscription change payment request payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
1306                        }
1307
1308                        $subscription_id = absint( $_POST['order_id'] ?? false );
1309                        $subscription    = $subscription_id ? wcs_get_subscription( $subscription_id ) : false;
1310
1311                        if ( ! $subscription ) {
1312                                throw new WC_Stripe_Exception( 'subscription_not_found', __( "We're not able to process this subscription change payment request payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
1313                        }
1314
1315                        $setup_intent_id = ( isset( $_POST['intent_id'] ) && is_string( $_POST['intent_id'] ) ) ? sanitize_text_field( wp_unslash( $_POST['intent_id'] ) ) : null;
1316
1317                        if ( empty( $setup_intent_id ) ) {
1318                                throw new WC_Stripe_Exception( 'intent_not_found', __( "We're not able to process this subscription change payment request payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
1319                        }
1320
1321                        $gateway = $this->get_upe_gateway();
1322                        $token   = $gateway->create_token_from_setup_intent( $setup_intent_id, $subscription->get_user() );
1323                        $notice  = __( 'Payment method updated.', 'woocommerce-gateway-stripe' );
1324
1325                        // Manually update the payment method for the subscription now that we have confirmed the payment method.
1326                        WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $subscription, $token->get_gateway_id() );
1327
1328                        // Set the new Stripe payment method ID and customer ID on the subscription.
1329                        $customer = new WC_Stripe_Customer( wp_get_current_user()->ID );
1330                        $gateway->set_customer_id_for_subscription( $subscription, $customer->get_id() );
1331                        $gateway->set_payment_method_id_for_subscription( $subscription, $token->get_token() );
1332
1333                        // Check if the subscription has the delayed update all flag and attempt to update all subscriptions after the intent has been confirmed. If successful, display the "updated all subscriptions" notice.
1334                        if ( WC_Subscriptions_Change_Payment_Gateway::will_subscription_update_all_payment_methods( $subscription ) && WC_Subscriptions_Change_Payment_Gateway::update_all_payment_methods_from_subscription( $subscription, $token->get_gateway_id() ) ) {
1335                                $notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-gateway-stripe' );
1336                        }
1337
1338                        wc_add_notice( $notice );
1339                        wp_send_json_success(
1340                                [
1341                                        'return_url' => $subscription->get_view_order_url(),
1342                                ],
1343                                200
1344                        );
1345                } catch ( WC_Stripe_Exception $e ) {
1346                        WC_Stripe_Logger::log( 'Change subscription payment method error: ' . $e->getMessage() );
1347                        wp_send_json_error(
1348                                [
1349                                        'error' => [
1350                                                'message' => $e->getLocalizedMessage(),
1351                                        ],
1352                                ]
1353                        );
1354                }
1355        }
1356
1357        /**
1358         * Determines whether the request needs to redirect customer off-site to authorize payment.
1359         * This is needed for the non-card UPE payment method (i.e. iDeal, giropay, etc.)
1360         *
1361         * @param array $payment_methods The list of payment methods used for the processing the payment.
1362         *
1363         * @return boolean True if the array consist of only one payment method and it isn't card, Boleto, Oxxo or Multibanco. False otherwise.
1364         */
1365        private function request_needs_redirection( $payment_methods ) {
1366                return 1 === count( $payment_methods ) && ! in_array( $payment_methods[0], [ WC_Stripe_Payment_Methods::CARD, WC_Stripe_Payment_Methods::BOLETO, WC_Stripe_Payment_Methods::OXXO, WC_Stripe_Payment_Methods::MULTIBANCO, WC_Stripe_Payment_Methods::CASHAPP_PAY ] );
1367        }
1368
1369        /**
1370         * Determines whether the intent needs to be confirmed later.
1371         *
1372         * Some payment methods such as CashApp, Boleto, Oxxo and Multibanco require the payment to be confirmed later when
1373         * displaying the voucher or QR code to the customer on the checkout or pay for order page.
1374         *
1375         * @param array $payment_methods The list of payment methods used for the processing the payment.
1376         *
1377         * @return boolean
1378         */
1379        private function is_delayed_confirmation_required( $payment_methods ) {
1380                return ! empty( array_intersect( $payment_methods, [ WC_Stripe_Payment_Methods::BOLETO, WC_Stripe_Payment_Methods::OXXO, WC_Stripe_Payment_Methods::MULTIBANCO, WC_Stripe_Payment_Methods::CASHAPP_PAY ] ) );
1381        }
1382
1383        /**
1384         * Check for a UPE redirect payment method on order received page or setup intent on payment methods page.
1385         *
1386         * @deprecated 8.3.0
1387         * @since 5.6.0
1388         * @version 5.6.0
1389         */
1390        public function maybe_process_upe_redirect() {
1391                wc_deprecated_function( __FUNCTION__, '8.3', 'WC_Stripe_Order_Handler::maybe_process_redirect_order' );
1392
1393                $gateway = $this->get_gateway();
1394                if ( is_a( $gateway, 'WC_Stripe_UPE_Payment_Gateway' ) ) {
1395                        $gateway->maybe_process_upe_redirect();
1396                }
1397        }
1398
1399        /**
1400         * Check if manual renewal is required for the payment method.
1401         *
1402         * @return bool
1403         */
1404        private function is_manual_renewal_required( $is_payment_method_reusable ) {
1405                return ( ! $is_payment_method_reusable && WC_Stripe_Subscriptions_Helper::is_manual_renewal_enabled() )
1406                        || WC_Stripe_Subscriptions_Helper::is_manual_renewal_required();
1407        }
1408}
Note: See TracBrowser for help on using the repository browser.