PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.4
Pay with Vipps and MobilePay for WooCommerce v6.2.4
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 5.4.3 5.4.2 All 187 releases
woo-vipps / recurring / includes / wc-vipps-recurring-checkout.php

wc-vipps-recurring-checkout.php in Pay with Vipps and MobilePay for WooCommerce 6.2.4, at recurring/includes/wc-vipps-recurring-checkout.php

1,308 lines 47.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 defined( 'ABSPATH' ) || exit;
4
5 class WC_Vipps_Recurring_Checkout {
6 private static ?WC_Vipps_Recurring_Checkout $instance = null;
7
8 private ?WC_Gateway_Vipps_Recurring $gateway = null;
9
10 /**
11 * Returns the *Singleton* instance of this class.
12 *
13 * @return WC_Vipps_Recurring_Checkout The *Singleton* instance.
14 */
15 public static function get_instance(): WC_Vipps_Recurring_Checkout {
16 if ( null === self::$instance ) {
17 self::$instance = new self();
18 }
19
20 return self::$instance;
21 }
22
23 public static function register_hooks(): void {
24 $instance = WC_Vipps_Recurring_Checkout::get_instance();
25 add_action( 'init', [ $instance, 'init' ] );
26 // Higher priority than the single payments Vipps plugin
27 add_filter( 'woocommerce_get_checkout_page_id', [ $instance, 'woocommerce_get_checkout_page_id' ], 20 );
28 add_action( 'template_redirect', [ $instance, 'template_redirect' ] );
29
30 if ( is_admin() ) {
31 add_action( 'admin_init', [ $instance, 'admin_init' ] );
32 }
33 }
34
35 public function gateway(): ?WC_Gateway_Vipps_Recurring {
36 if ( $this->gateway ) {
37 return $this->gateway;
38 }
39
40 $this->gateway = WC_Vipps_Recurring::get_instance()->gateway();
41
42 return $this->gateway;
43 }
44
45 public function maybe_load_cart(): void {
46 if ( version_compare( WC_VERSION, '3.6.0', '>=' ) && WC()->is_rest_api_request() ) {
47 if ( empty( $_SERVER['REQUEST_URI'] ) ) {
48 return;
49 }
50
51 $rest_prefix = 'vipps-mobilepay-recurring/v1/';
52 $req_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
53
54 $is_my_endpoint = ( false !== strpos( $req_uri, $rest_prefix ) );
55
56 if ( ! $is_my_endpoint ) {
57 return;
58 }
59
60 require_once WC_ABSPATH . 'includes/wc-cart-functions.php';
61 require_once WC_ABSPATH . 'includes/wc-notice-functions.php';
62
63 if ( null === WC()->session ) {
64 $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
65
66 // Prefix session class with global namespace if not already namespaced
67 if ( false === strpos( $session_class, '\\' ) ) {
68 $session_class = '\\' . $session_class;
69 }
70
71 WC()->session = new $session_class();
72 WC()->session->init();
73 }
74
75 /**
76 * For logged in customers, pull data from their account rather than the
77 * session which may contain incomplete data.
78 */
79 if ( is_null( WC()->customer ) ) {
80 if ( is_user_logged_in() ) {
81 WC()->customer = new WC_Customer( get_current_user_id() );
82 } else {
83 WC()->customer = new WC_Customer( get_current_user_id(), true );
84 }
85
86 // Customer should be saved during shutdown.
87 add_action( 'shutdown', array( WC()->customer, 'save' ), 10 );
88 }
89
90 // Load Cart.
91 if ( null === WC()->cart ) {
92 WC()->cart = new WC_Cart();
93 }
94 }
95 }
96
97 public function init(): void {
98 require_once __DIR__ . '/wc-vipps-recurring-checkout-rest-api.php';
99 WC_Vipps_Recurring_Checkout_Rest_Api::get_instance();
100
101 add_action( 'wp_loaded', [ $this, 'maybe_load_cart' ], 5 );
102
103 // Prevent previews and prefetches of the Vipps Checkout page starting and creating orders
104 add_action( 'wp_head', [ $this, 'wp_head' ] );
105
106 // The Vipps MobilePay Checkout feature which overrides the normal checkout process uses a shortcode
107 add_shortcode( 'vipps_recurring_checkout', [ $this, 'shortcode' ] );
108
109 add_action( 'wc_vipps_recurring_before_cron_check_order_status', [ $this, 'check_order_status' ] );
110 add_action( 'wc_vipps_recurring_before_rest_api_check_order_status', [ $this, 'check_order_status' ] );
111
112 // For Checkout, we need to know any time and as soon as the cart changes, so fold all the events into a single one
113 add_action( 'woocommerce_add_to_cart', function () {
114 do_action( 'vipps_recurring_cart_changed', 'woocommerce_add_to_cart' );
115 }, 10, 0 );
116
117 // Cart coupon applied
118 add_action( 'woocommerce_applied_coupon', function () {
119 do_action( 'vipps_recurring_cart_changed', 'woocommerce_applied_coupon' );
120 }, 10, 0 );
121
122 // Cart emptied
123 add_action( 'woocommerce_cart_emptied', function () {
124 do_action( 'vipps_recurring_cart_changed', 'woocommerce_cart_emptied' );
125 }, 10, 0 );
126
127 // After updating quantities
128 add_action( 'woocommerce_after_cart_item_quantity_update', function () {
129 do_action( 'vipps_recurring_cart_changed', 'woocommerce_after_cart_item_quantity_update' );
130 }, 10, 0 );
131
132 // Blocks and ajax
133 add_action( 'woocommerce_cart_item_removed', function () {
134 do_action( 'vipps_recurring_cart_changed', 'woocommerce_cart_item_removed' );
135 }, 10, 0 );
136
137 // Restore deleted entry
138 add_action( 'woocommerce_cart_item_restored', function () {
139 do_action( 'vipps_recurring_cart_changed', 'woocommerce_cart_item_restored' );
140 }, 10, 0 );
141
142 // Normal cart form update
143 add_filter( 'woocommerce_update_cart_action_cart_updated', function ( $updated ) {
144 do_action( 'vipps_recurring_cart_changed', 'woocommerce_update_cart_action_cart_updated' );
145
146 return $updated;
147 } );
148
149 // Then handle the actual cart change
150 add_action( 'vipps_recurring_cart_changed', [ $this, 'cart_changed' ] );
151
152 add_action( 'wc_vipps_recurring_checkout_callback', [ $this, 'handle_callback' ], 10, 2 );
153
154 // Handle cancelled orders
155 add_action( 'wc_vipps_recurring_check_charge_status_no_agreement', [ $this, 'maybe_cancel_initial_order' ] );
156
157 add_filter( 'wcs_user_has_subscription', [ $this, 'user_has_subscription' ], 10, 4 );
158 }
159
160 public function admin_init(): void {
161 // Checkout page
162 add_filter( 'woocommerce_settings_pages', array( $this, 'woocommerce_settings_pages' ) );
163 }
164
165 /**
166 * @throws WC_Vipps_Recurring_Exception
167 * @throws WC_Vipps_Recurring_Temporary_Exception
168 * @throws WC_Vipps_Recurring_Config_Exception
169 */
170 public function maybe_create_session(): array {
171 $redirect_url = null;
172 $token = null;
173 $url = null;
174
175 $session = WC_Vipps_Recurring_Checkout::get_instance()->current_pending_session();
176
177 if ( isset( $session['redirect_url'] ) ) {
178 $redirect_url = $session['redirect_url'];
179 }
180
181 if ( isset( $session['session']['token'] ) ) {
182 $token = $session['session']['token'];
183 $src = $session['session']['checkoutFrontendUrl'];
184 $url = $src;
185 }
186
187 if ( $url ) {
188 $pending_order_id = WC_Vipps_Recurring_Helper::get_checkout_pending_order_id();
189
190 return [
191 'success' => true,
192 'src' => $url,
193 'redirect_url' => $redirect_url,
194 'token' => $token,
195 'order_id' => $pending_order_id
196 ];
197 }
198
199 $session = null;
200
201 try {
202 $partial_order_id = WC_Gateway_Vipps_Recurring::get_instance()->create_partial_order( true );
203
204 $order = wc_get_order( $partial_order_id );
205 $auth_token = WC_Gateway_Vipps_Recurring::get_instance()->api->generate_idempotency_key();
206
207 $order->update_meta_data( WC_Vipps_Recurring_Helper::META_ORDER_EXPRESS_AUTH_TOKEN, $auth_token );
208 $order->update_meta_data( WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_ORIGINAL_CUSTOMER_ID, $order->get_customer_id( 'edit' ) );
209 $order->save();
210
211 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_CHECKOUT_PENDING_ORDER_ID, $partial_order_id );
212 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_ORDER_EXPRESS_AUTH_TOKEN, $auth_token );
213
214 do_action( 'wc_vipps_recurring_checkout_order_created', $order );
215 } catch ( Exception $exception ) {
216 return [
217 'success' => false,
218 'msg' => $exception->getMessage(),
219 'src' => null,
220 'redirect_url' => null,
221 'order_id' => 0
222 ];
223 }
224
225 $order = wc_get_order( $partial_order_id );
226
227 $session_orders = WC()->session->get( WC_Vipps_Recurring_Helper::SESSION_ORDERS );
228 if ( ! $session_orders ) {
229 $session_orders = [];
230 }
231
232 $session_orders[ $partial_order_id ] = 1;
233 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_PENDING_ORDER_ID, $partial_order_id );
234 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_ORDERS, $session_orders );
235
236 $customer_id = get_current_user_id();
237 if ( $customer_id ) {
238 $customer = new WC_Customer( $customer_id );
239 } else {
240 $customer = WC()->customer;
241 }
242
243 if ( $customer ) {
244 $customer_info['email'] = $customer->get_billing_email();
245 $customer_info['firstName'] = $customer->get_billing_first_name();
246 $customer_info['lastName'] = $customer->get_billing_last_name();
247 $customer_info['streetAddress'] = $customer->get_billing_address_1();
248 $address2 = trim( $customer->get_billing_address_2() );
249
250 if ( ! empty( $address2 ) ) {
251 $customer_info['streetAddress'] = $customer_info['streetAddress'] . ", " . $address2;
252 }
253 $customer_info['city'] = $customer->get_billing_city();
254 $customer_info['postalCode'] = $customer->get_billing_postcode();
255 $customer_info['country'] = $customer->get_billing_country();
256
257 // Currently Vipps requires all phone numbers to have area codes and NO +. We can't guarantee that at all, but try for Norway
258 $normalized_phone_number = WC_Vipps_Recurring_Helper::normalize_phone_number( $customer->get_billing_phone(), $customer_info['country'] );
259 if ( $normalized_phone_number ) {
260 $customer_info['phoneNumber'] = $normalized_phone_number;
261 }
262 }
263
264 $keys = [ 'firstName', 'lastName', 'streetAddress', 'postalCode', 'country', 'phoneNumber' ];
265 foreach ( $keys as $k ) {
266 if ( empty( $customer_info[ $k ] ) ) {
267 $customer_info = [];
268 break;
269 }
270 }
271 $customer_info = apply_filters( 'wc_vipps_recurring_customer_info', $customer_info, $order );
272
273 // todo: throw an error if we try to purchase a product with a location based shipping method?
274 try {
275 $checkout = WC_Vipps_Recurring_Checkout::get_instance();
276 $gateway = $checkout->gateway();
277
278 // hack - fake "Anonymous Vipps/MobilePay User"
279 $fake_user = false;
280 if ( ! $order->get_customer_id( 'edit' ) ) {
281 $fake_user = true;
282
283 $order->set_customer_id( $this->gateway()->create_or_get_anonymous_system_customer()->get_id() );
284 $order->save();
285 }
286
287 // Check if we already have a subscription on this order, otherwise create one
288 if ( wcs_order_contains_subscription( $order, 'any' ) ) {
289 $subscriptions = wcs_get_subscriptions_for_order( $order );
290 } else {
291 $subscriptions = $gateway->create_partial_subscriptions_from_order( $order );
292 }
293
294 // reset hack
295 if ( $fake_user ) {
296 $order->set_customer_id( 0 );
297 $order->save();
298 }
299
300 $subscription = array_pop( $subscriptions );
301
302 // Remove this action to avoid making an unnecessary API request
303 remove_action( 'woocommerce_order_after_calculate_totals', [
304 $this->gateway(),
305 'update_agreement_price_in_app'
306 ] );
307
308 $subscription->calculate_totals();
309 $subscription->save();
310
311 $agreement = $gateway->create_vipps_agreement_from_order( $order, $subscription );
312
313 $checkout_subscription = ( new WC_Vipps_Checkout_Session_Subscription() )
314 ->set_amount(
315 ( new WC_Vipps_Checkout_Session_Amount() )
316 ->set_value( $agreement->pricing->amount )
317 ->set_currency( $agreement->pricing->currency )
318 )
319 ->set_product_name( $agreement->product_name )
320 ->set_interval( $agreement->interval )
321 ->set_merchant_agreement_url( $agreement->merchant_agreement_url );
322
323 if ( $agreement->campaign ) {
324 $checkout_subscription = $checkout_subscription->set_campaign( $agreement->campaign );
325 }
326
327 if ( $agreement->product_description ) {
328 $checkout_subscription = $checkout_subscription->set_product_description( $agreement->product_description );
329 }
330
331 $configuration = ( new WC_Vipps_Checkout_Session_Configuration() )
332 ->set_user_flow( WC_Vipps_Checkout_Session_Configuration::USER_FLOW_WEB_REDIRECT )
333 ->set_customer_interaction( WC_Vipps_Checkout_Session_Configuration::CUSTOMER_INTERACTION_NOT_PRESENT )
334 ->set_elements( WC_Vipps_Checkout_Session_Configuration::ELEMENTS_FULL )
335 ->set_require_user_info( empty( $customer->email ) )
336 ->set_show_order_summary( true );
337
338 $countries = array_keys( ( new WC_Countries() )->get_allowed_countries() );
339 $allowed_countries = apply_filters( 'woo_vipps_recurring_checkout_countries', $countries, $order->get_id() );
340 if ( $allowed_countries ) {
341 $configuration->set_countries( [ 'supported' => $allowed_countries ] );
342 }
343
344 $customer = new WC_Vipps_Checkout_Session_Customer( $customer_info );
345
346 // Create a checkout session dto
347 $checkout_session = ( new WC_Vipps_Checkout_Session() )
348 ->set_type( WC_Vipps_Checkout_Session::TYPE_SUBSCRIPTION )
349 ->set_subscription( $checkout_subscription )
350 ->set_merchant_info(
351 ( new WC_Vipps_Checkout_Session_Merchant_Info() )
352 ->set_callback_url( $gateway->webhook_callback_url() )
353 ->set_return_url( $agreement->merchant_redirect_url )
354 ->set_callback_authorization_token( $auth_token )
355 )
356 ->set_prefill_customer( $customer )
357 ->set_configuration( $configuration );
358
359 if ( $agreement->initial_charge ) {
360 $reference = $agreement->initial_charge->order_id;
361
362 $checkout_transaction = ( new WC_Vipps_Checkout_Session_Transaction() )
363 ->set_reference( $reference )
364 ->set_amount(
365 ( new WC_Vipps_Checkout_Session_Amount() )
366 ->set_value( $agreement->initial_charge->amount )
367 ->set_currency( $agreement->pricing->currency )
368 )
369 ->set_order_summary( $checkout->make_order_summary( $order ) );
370
371 if ( $agreement->initial_charge->description ) {
372 $checkout_transaction = $checkout_transaction->set_payment_description( $agreement->initial_charge->description );
373 }
374
375 $checkout_session = $checkout_session->set_transaction( $checkout_transaction );
376
377 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_ID, $reference );
378 }
379
380 $checkout_session = apply_filters( 'wc_vipps_recurring_checkout_session', $checkout_session, $order );
381
382 $session = WC_Gateway_Vipps_Recurring::get_instance()->api->checkout_initiate( $checkout_session );
383
384 $order = wc_get_order( $partial_order_id );
385 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING, true );
386 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_INITIAL, true );
387 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_SESSION, $session->to_array() );
388
389 $session_poll = WC_Gateway_Vipps_Recurring::get_instance()->api->checkout_poll( $session->polling_url );
390 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_SESSION_ID, $session_poll['sessionId'] );
391
392 $order->add_order_note( __( 'Vipps/MobilePay recurring checkout payment initiated', 'woo-vipps' ) );
393 $order->add_order_note( __( 'Customer passed to Vipps/MobilePay checkout', 'woo-vipps' ) );
394 $order->save();
395
396 $token = $session->token;
397 $src = $session->checkout_frontend_url;
398 $url = $src;
399 } catch ( Exception $e ) {
400 WC_Vipps_Recurring_Logger::log( sprintf( "Could not initiate Vipps/MobilePay checkout session: %s", $e->getMessage() ) );
401
402 return [
403 'success' => false,
404 'msg' => $e->getMessage(),
405 'src' => null,
406 'redirect_url' => null,
407 'order_id' => $partial_order_id
408 ];
409 }
410
411 if ( $url || $redirect_url ) {
412 return [
413 'success' => true,
414 'msg' => 'session started',
415 'src' => $url,
416 'redirect_url' => $redirect_url,
417 'token' => $token,
418 'order_id' => $partial_order_id
419 ];
420 }
421
422 return [
423 'success' => false,
424 'msg' => __( 'Could not start Vipps/MobilePay checkout session', 'woo-vipps' ),
425 'src' => $url,
426 'redirect_url' => $redirect_url,
427 'order_id' => $partial_order_id
428 ];
429 }
430
431 public function maybe_login_checkout_user( int $order_id, ?string $key = null ): void {
432 if ( ! $key ) {
433 return;
434 }
435
436 $order = wc_get_order( $order_id );
437 if ( ! $order ) {
438 return;
439 }
440
441 $order_key_db = $order->get_order_key( 'code' );
442 if ( ! hash_equals( $order_key_db, $key ) ) {
443 return;
444 }
445
446 if ( ! WC()->session ) {
447 return;
448 }
449
450 // Only the browser session that created this Checkout order may log in its customer.
451 $session_auth_token = WC()->session->get( WC_Vipps_Recurring_Helper::SESSION_ORDER_EXPRESS_AUTH_TOKEN );
452 $order_auth_token = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_EXPRESS_AUTH_TOKEN );
453
454 if ( ! is_string( $order_auth_token ) || ! is_string( $session_auth_token )
455 || ! $order_auth_token || ! $session_auth_token
456 || ! hash_equals( $order_auth_token, $session_auth_token ) ) {
457 return;
458 }
459
460 if ( is_user_logged_in() ) {
461 $current_user_id = get_current_user_id();
462 $order_user_id = $order->get_customer_id( 'edit' );
463
464 if ( $current_user_id !== $order_user_id ) {
465 $this->assign_order_and_subscriptions_to_customer( $order, $current_user_id );
466 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Assigned the Checkout order to returning logged-in customer %s instead of customer %s', $order_id, $current_user_id, $order_user_id ) );
467 }
468
469 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_ORDER_EXPRESS_AUTH_TOKEN, null );
470
471 return;
472 }
473
474 $user = $order->get_user();
475 if ( ! $user ) {
476 return;
477 }
478
479 $roles = $user->roles;
480 $is_customer = ! empty( array_intersect( [ 'customer', 'subscriber' ], $roles ) );
481 if ( ! $is_customer || user_can( $user, 'manage_woocommerce' ) || user_can( $user, 'manage_options' ) ) {
482 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Refusing to log in a privileged or non-customer user on the Checkout return', $order_id ) );
483 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_ORDER_EXPRESS_AUTH_TOKEN, null );
484
485 return;
486 }
487
488 wc_set_customer_auth_cookie( $user->ID );
489 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_ORDER_EXPRESS_AUTH_TOKEN, null );
490 }
491
492 private function session_owns_checkout_order( WC_Order $order ): bool {
493 if ( ! WC()->session ) {
494 return false;
495 }
496
497 $session_auth_token = WC()->session->get( WC_Vipps_Recurring_Helper::SESSION_ORDER_EXPRESS_AUTH_TOKEN );
498 $order_auth_token = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_EXPRESS_AUTH_TOKEN );
499
500 return is_string( $order_auth_token ) && is_string( $session_auth_token )
501 && $order_auth_token && $session_auth_token
502 && hash_equals( $order_auth_token, $session_auth_token );
503 }
504
505 private function assign_order_and_subscriptions_to_customer( WC_Order $order, int $customer_id ): void {
506 $order->set_customer_id( $customer_id );
507 $order->save();
508
509 $subscriptions = wcs_get_subscriptions_for_order( $order );
510 foreach ( $subscriptions as $subscription ) {
511 $subscription->set_customer_id( $customer_id );
512 $subscription->save();
513 }
514 }
515
516 private function prepare_pending_order_for_current_customer( WC_Order $order ): bool {
517 $current_user_id = get_current_user_id();
518 $order_user_id = (int) $order->get_customer_id( 'edit' );
519 $started_as_guest = $order->meta_exists( WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_ORIGINAL_CUSTOMER_ID )
520 && ! (int) WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_ORIGINAL_CUSTOMER_ID );
521
522 if ( ! $current_user_id ) {
523 return ! $order_user_id || ( $started_as_guest && $this->session_owns_checkout_order( $order ) );
524 }
525
526 if ( $order_user_id && $order_user_id !== $current_user_id ) {
527 if ( ! $this->session_owns_checkout_order( $order ) ) {
528 return false;
529 }
530
531 $this->assign_order_and_subscriptions_to_customer( $order, $current_user_id );
532
533 return true;
534 }
535
536 if ( $order_user_id === $current_user_id ) {
537 return true;
538 }
539
540 if ( $order->meta_exists( WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_ORIGINAL_CUSTOMER_ID )
541 && ! $this->session_owns_checkout_order( $order ) ) {
542 return false;
543 }
544
545 $this->assign_order_and_subscriptions_to_customer( $order, $current_user_id );
546
547 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Assigned a migrated guest Checkout order to logged-in customer %s', $order->get_id(), $current_user_id ) );
548
549 return true;
550 }
551
552 public function cart_changed( string $source ): void {
553 $pending_order_id = is_a( WC()->session, 'WC_Session' ) ? WC()->session->get( WC_Vipps_Recurring_Helper::SESSION_CHECKOUT_PENDING_ORDER_ID ) : false;
554 $order = $pending_order_id ? wc_get_order( $pending_order_id ) : null;
555
556 if ( ! $order ) {
557 return;
558 }
559
560 WC_Vipps_Recurring_Logger::log( sprintf( "Checkout cart changed while session %d in progress, attempting to cancel. Source: %s", $order->get_id(), $source ) );
561 $this->abandon_checkout_order( $order );
562 }
563
564 /**
565 * @throws WC_Vipps_Recurring_Config_Exception
566 * @throws WC_Data_Exception
567 * @throws WC_Vipps_Recurring_Exception
568 * @throws WC_Vipps_Recurring_Temporary_Exception
569 */
570 public function check_order_status( $order_id ): void {
571 $lock_name = "vipps_recurring_checkout_check_order_status_$order_id";
572 $lock = get_transient( $lock_name );
573
574 if ( $lock ) {
575 return;
576 }
577
578 set_transient( $lock_name, uniqid( '', true ), 5 );
579
580 $order = wc_get_order( $order_id );
581
582 if ( ! WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_IS_CHECKOUT ) ) {
583 return;
584 }
585
586 $session = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_SESSION );
587 if ( ! is_array( $session ) ) {
588 return;
589 }
590
591 $session = $this->gateway()->api->checkout_poll( $session['pollingUrl'] );
592
593 $this->handle_payment( $order, $session );
594 }
595
596 public function woocommerce_settings_pages( $settings ) {
597 $checkout_enabled = get_option( WC_Vipps_Recurring_Helper::OPTION_CHECKOUT_ENABLED, false );
598 if ( ! $checkout_enabled ) {
599 return $settings;
600 }
601
602 // Find out where the end of the section advanced_page_options is
603 $i = 0;
604 $count = count( $settings );
605
606 for ( ; $i < $count; $i ++ ) {
607 if ( $settings[ $i ]['type'] === 'sectionend' && $settings[ $i ]['id'] === 'advanced_page_options' ) {
608 break;
609 }
610 }
611
612 if ( $i < $count ) {
613 array_splice( $settings, $i, 0, [
614 [
615 'title' => __( 'Vipps/MobilePay Recurring Checkout Page', 'woo-vipps' ),
616 'desc' => __( 'This page is used for the alternative Vipps/MobilePay checkout page, which you can choose to use instead of the normal WooCommerce checkout page. ', 'woo-vipps' ) . sprintf( __( 'Page contents: [%1$s]', 'woocommerce' ), 'vipps_recurring_checkout' ),
617 'id' => 'woocommerce_vipps_recurring_checkout_page_id',
618 'type' => 'single_select_page_with_search',
619 'default' => '',
620 'class' => 'wc-page-search',
621 'css' => 'min-width:300px;',
622 'args' => [
623 'exclude' =>
624 [
625 wc_get_page_id( 'myaccount' ),
626 ],
627 ],
628 'desc_tip' => true,
629 'autoload' => false,
630 ]
631 ] );
632 }
633
634 return $settings;
635 }
636
637 public function woocommerce_get_checkout_page_id( $id ): int {
638 $checkout_enabled = get_option( WC_Vipps_Recurring_Helper::OPTION_CHECKOUT_ENABLED, false );
639
640 if ( ! $checkout_enabled ) {
641 return $id;
642 }
643
644 $checkout_id = $this->gateway()->checkout_is_available();
645 if ( $checkout_id ) {
646 return $checkout_id;
647 }
648
649 return $id;
650 }
651
652 public function template_redirect(): void {
653 global $post;
654
655 if ( $post && is_page() && has_shortcode( $post->post_content, 'vipps_recurring_checkout' ) ) {
656 $this->register_scripts();
657
658 add_filter( 'woocommerce_is_checkout', '__return_true' );
659
660 add_filter( 'body_class', function ( $classes ) {
661 $classes[] = 'vipps-recurring-checkout';
662 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site
663
664 return apply_filters( 'wc_vipps_recurring_checkout_body_class', $classes );
665 } );
666
667 // Suppress the title for this page
668 $post_to_hide_title_for = $post->ID;
669 add_filter( 'the_title', function ( $title, $postid = 0 ) use ( $post_to_hide_title_for ) {
670 if ( ! is_admin() && $postid == $post_to_hide_title_for && is_singular() && in_the_loop() ) {
671 $title = "";
672 }
673
674 return $title;
675 }, 10, 2 );
676
677 wc_nocache_headers();
678 }
679 }
680
681 public function wp_head(): void {
682 // If we have a Vipps MobilePay Checkout page, stop iOS from giving previews of it that
683 // starts the session - iOS should use the visibility API of the browser for this, but it doesn't as of 2021-11-11
684 $checkout_id = wc_get_page_id( 'vipps_recurring_checkout' );
685 if ( $checkout_id ) {
686 $url = get_permalink( $checkout_id );
687 echo "<style> a[href=\"$url\"] { -webkit-touch-callout: none; } </style>\n";
688 }
689 }
690
691 public function register_scripts(): void {
692 $sdk_url = 'https://checkout.vipps.no/vippsCheckoutSDK.js';
693 wp_register_script( 'woo-vipps-recurring-sdk', $sdk_url );
694
695 // Register our React component
696 wp_enqueue_style(
697 'woo-vipps-recurring-checkout',
698
699 WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/build/checkout.css', [],
700 filemtime( WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/checkout.css' )
701 );
702
703 $asset = require WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/checkout.asset.php';
704
705 wp_enqueue_script(
706 'woo-vipps-recurring-checkout',
707 WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/build/checkout.js',
708 array_merge( $asset['dependencies'], [ 'woo-vipps-recurring', 'woo-vipps-recurring-sdk' ] ),
709 filemtime( WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/checkout.js' ),
710 true
711 );
712 }
713
714 /**
715 * @throws WC_Vipps_Recurring_Exception
716 * @throws WC_Vipps_Recurring_Temporary_Exception
717 * @throws WC_Vipps_Recurring_Config_Exception
718 */
719 public function shortcode() {
720 global $wp;
721
722 if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
723 return false;
724 }
725
726 // No point in expanding this unless we are actually doing the checkout. IOK 2021-09-03
727 wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
728 add_filter( 'wc_vipps_recurring_is_vipps_checkout', '__return_true' );
729
730 if ( is_wc_endpoint_url( 'order-received' ) ) {
731 $order_id = absint( $wp->query_vars['order-received'] );
732 $key = isset( $_GET['key'] ) && is_string( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : null;
733 $this->maybe_login_checkout_user( $order_id, $key );
734 }
735
736 // Defer to the normal code for endpoints IOK 2022-12-09
737 if ( is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' ) ) {
738 return do_shortcode( "[woocommerce_checkout]" );
739 }
740
741 if ( ! WC()->cart || WC()->cart->is_empty() ) {
742 $this->abandon_checkout_order( false );
743 ob_start();
744 wc_get_template( 'cart/cart-empty.php' );
745
746 return ob_get_clean();
747 }
748
749 // Previously registered, now enqueue this script which should then appear in the footer.
750 wp_enqueue_script( 'woo-vipps-recurring-checkout' );
751
752 do_action( 'vipps_recurring_checkout_before_get_session' );
753
754 // Localize script with variables we need to reveal to the frontend
755 $data = $this->current_pending_session();
756 if ( empty( $data['session'] ) ) {
757 $data['session'] = $this->maybe_create_session();
758 }
759
760 $data['language'] = Vipps::instance()->get_customer_language();
761
762 wp_add_inline_script( 'woo-vipps-recurring-checkout', 'window.VippsRecurringCheckout = ' . wp_json_encode( $data ), 'before' );
763
764 return '<div id="vipps-mobilepay-recurring-checkout"></div>';
765 }
766
767 public function make_order_summary( $order ): WC_Vipps_Checkout_Session_Transaction_Order_Summary {
768 $order_lines = [];
769
770 $bottom_line = ( new WC_Vipps_Checkout_Session_Transaction_Order_Summary_Bottom_Line() )
771 ->set_currency( $order->get_currency() )
772 ->set_gift_card_amount( apply_filters( 'wc_vipps_recurring_order_gift_card_amount', 0, $order ) * 100 )
773 ->set_tip_amount( apply_filters( 'wc_vipps_recurring_order_tip_amount', 0, $order ) * 100 )
774 ->set_terminal_id( apply_filters( 'wc_vipps_recurring_order_terminal_id', 'woocommerce', $order ) )
775 ->set_receipt_number( strval( WC_Vipps_Recurring_Helper::get_id( $order ) ) );
776
777 foreach ( $order->get_items() as $order_item ) {
778 $order_line = [];
779 $product_id = $order_item->get_product_id(); // sku can be tricky
780 $total_no_tax = $order_item->get_total();
781 $tax = $order_item->get_total_tax();
782 $total = $tax + $total_no_tax;
783 $subtotal_no_tax = $order_item->get_subtotal();
784 $subtotal_tax = $order_item->get_subtotal_tax();
785 $subtotal = $subtotal_no_tax + $subtotal_tax;
786 $quantity = $order_item->get_quantity();
787 $unit_price = $subtotal / $quantity;
788
789 // Must do this to avoid rounding errors, since we get floats instead of money here :(
790 $discount = round( 100 * $subtotal ) - round( 100 * $total );
791 if ( $discount < 0 ) {
792 $discount = 0;
793 }
794
795 $product = wc_get_product( $product_id );
796 $url = home_url( "/" );
797 if ( $product ) {
798 $url = get_permalink( $product_id );
799 }
800
801 if ( $subtotal_no_tax == 0 ) {
802 $tax_percentage = 0;
803 } else {
804 $tax_percentage = ( ( $subtotal - $subtotal_no_tax ) / $subtotal_no_tax ) * 100;
805 }
806 $tax_percentage = abs( round( $tax_percentage ) );
807
808 $unit_info = [];
809 $order_line['name'] = $order_item->get_name();
810 $order_line['id'] = strval( $product_id );
811 $order_line['totalAmount'] = round( $total * 100 );
812 $order_line['totalAmountExcludingTax'] = round( $total_no_tax * 100 );
813 $order_line['totalTaxAmount'] = round( $tax * 100 );
814 $order_line['taxRate'] = round( $tax_percentage * 100 );
815
816 $unit_info['unitPrice'] = round( $unit_price * 100 );
817 $unit_info['quantity'] = strval( $quantity );
818 $unit_info['quantityUnit'] = 'PCS';
819 $order_line['unitInfo'] = $unit_info;
820 $order_line['discount'] = $discount;
821 $order_line['productUrl'] = $url;
822 $order_line['isShipping'] = false;
823
824 $order_lines[] = $order_line;
825 }
826
827 foreach ( $order->get_items( 'fee' ) as $order_item ) {
828 $order_line = [];
829 $total_no_tax = $order_item->get_total();
830 $tax = $order_item->get_total_tax();
831 $total = $tax + $total_no_tax;
832 $tax_percentage = ( ( $total - $total_no_tax ) / $total_no_tax ) * 100;
833 $tax_percentage = abs( round( $tax_percentage ) );
834 $order_line['name'] = $order_item->get_name();
835 $order_line['id'] = substr( sanitize_title( $order_line['name'] ), 0, 254 );
836 $order_line['totalAmount'] = round( $total * 100 );
837 $order_line['totalAmountExcludingTax'] = round( $total_no_tax * 100 );
838 $order_line['totalTaxAmount'] = round( $tax * 100 );
839 $order_line['discount'] = 0;
840 $order_line['taxRate'] = round( $tax_percentage * 100 );
841
842 $order_lines[] = $order_line;
843 }
844
845 // Handle shipping
846 foreach ( $order->get_items( 'shipping' ) as $order_item ) {
847 $order_line = [];
848 $order_line['name'] = $order_item->get_name();
849 $order_line['id'] = strval( $order_item->get_method_id() );
850 if ( method_exists( $order_item, 'get_instance_id' ) ) {
851 $order_line['id'] .= ":" . $order_item->get_instance_id();
852 }
853
854 $total_no_tax = $order_item->get_total();
855 $tax = $order_item->get_total_tax();
856 $total = $tax + $total_no_tax;
857 $subtotal_no_tax = $total_no_tax;
858 $subtotal_tax = $tax;
859 $subtotal = $subtotal_no_tax + $subtotal_tax;
860
861 if ( $subtotal_no_tax == 0 ) {
862 $tax_percentage = 0;
863 } else {
864 $tax_percentage = ( ( $subtotal - $subtotal_no_tax ) / $subtotal_no_tax ) * 100;
865 }
866 $tax_percentage = abs( round( $tax_percentage ) );
867
868 $order_line['totalAmount'] = round( $total * 100 );
869 $order_line['totalAmountExcludingTax'] = round( $total_no_tax * 100 );
870 $order_line['totalTaxAmount'] = round( $tax * 100 );
871 $order_line['taxRate'] = round( $tax_percentage * 100 );
872
873 $unit_info = [];
874 $unit_info['unitPrice'] = round( $total * 100 );
875 $unit_info['quantity'] = strval( 1 );
876 $unit_info['quantityUnit'] = 'PCS';
877
878 $order_line['unitInfo'] = $unit_info;
879 $discount = 0;
880 $order_line['discount'] = $discount;
881 $order_line['isShipping'] = true;
882
883 $order_lines[] = $order_line;
884 }
885
886 return ( new WC_Vipps_Checkout_Session_Transaction_Order_Summary() )
887 ->set_order_lines( $order_lines )
888 ->set_order_bottom_line( $bottom_line );
889 }
890
891 /**
892 * @throws WC_Vipps_Recurring_Config_Exception
893 * @throws WC_Vipps_Recurring_Exception
894 * @throws WC_Vipps_Recurring_Temporary_Exception
895 */
896 public function current_pending_session(): array {
897 // If this is set, this is a currently pending order which is maybe still valid
898 $pending_order_id = WC_Vipps_Recurring_Helper::get_checkout_pending_order_id();
899 $order = $pending_order_id ? wc_get_order( $pending_order_id ) : null;
900
901 if ( $order && ! $this->prepare_pending_order_for_current_customer( $order ) ) {
902 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Ignoring a pending Checkout order that does not belong to the current customer', $order->get_id() ) );
903 $this->abandon_checkout_order( false );
904 $order = null;
905 $pending_order_id = false;
906 }
907
908 # If we do have an order, we need to check if it is 'pending', and if not, we have to check its payment status
909 $payment_status = null;
910 $redirect = null;
911
912 if ( $order ) {
913 if ( $order->get_status() === 'pending' ) {
914 $payment_status = 'INITIATED'; // Just assume this for now
915 } else {
916 $payment_status = $this->gateway()->check_charge_status( $pending_order_id, true ) ?? 'UNKNOWN';
917 }
918
919 if ( $payment_status === 'SUCCESS' ) {
920 $redirect = apply_filters( 'wc_vipps_recurring_merchant_redirect_url', WC_Vipps_Recurring_Helper::get_payment_redirect_url( $order ) );
921 }
922 }
923
924 if ( in_array( $payment_status, [ 'authorized', 'complete' ] ) ) {
925 $this->abandon_checkout_order( false );
926 } elseif ( $payment_status == 'cancelled' ) {
927 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Vipps/MobilePay checkout session cancelled (pending session)", $order->get_id() ) );
928
929 // This will mostly just wipe the session.
930 $this->abandon_checkout_order( $order );
931 }
932
933 // Now if we don't have an order right now, we should not have a session either, so fix that
934 if ( ! $order ) {
935 $this->abandon_checkout_order( false );
936 }
937
938 // Now check the orders vipps session if it exist
939 $session = $order ? $order->get_meta( WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_SESSION ) : false;
940
941 // A single word or array containing session data, containing token and frontendFrameUrl
942 // ERROR EXPIRED FAILED
943 $session_status = $session ? $this->get_checkout_status( $session ) : null;
944
945 // If this is the case, there is no redirect, but the session is gone, so wipe the order and session.
946 if ( in_array( $session_status, [ 'ERROR', 'EXPIRED', 'FAILED' ] ) ) {
947 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Vipps/MobilePay checkout session is gone", $order->get_id() ) );
948 $this->abandon_checkout_order( $order );
949 }
950
951 // This will return either a valid vipps session, nothing, or redirect.
952 return [
953 'success' => (bool) $order,
954 'order' => $order ? $order->get_id() : false,
955 'session' => $session,
956 'redirect_url' => $redirect
957 ];
958 }
959
960 /**
961 * @param $session
962 *
963 * @return string
964 */
965 public function get_checkout_status( $session ): string {
966 if ( $session && isset( $session['token'] ) ) {
967 try {
968 WC_Vipps_Recurring_Logger::log( "Polling checkout from get_checkout_status" );
969 $response = $this->gateway()->api->checkout_poll( $session['pollingUrl'] );
970
971 return $response['sessionState'] ?? "PaymentInitiated";
972 } catch ( WC_Vipps_Recurring_Exception $e ) {
973 if ( $e->response_code == 400 ) {
974 return 'PaymentInitiated';
975 } else if ( $e->response_code == 404 ) {
976 return 'SessionExpired';
977 } else {
978 WC_Vipps_Recurring_Logger::log( sprintf( "Error polling status - error message %s", $e->getMessage() ) );
979
980 return 'ERROR';
981 }
982 } catch ( Exception $e ) {
983 WC_Vipps_Recurring_Logger::log( sprintf( "Error polling status - error message %s", $e->getMessage() ) );
984
985 return 'ERROR';
986 }
987 }
988
989 return "ERROR";
990 }
991
992 public function abandon_checkout_order( $order ) {
993 if ( WC()->session ) {
994 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_CHECKOUT_PENDING_ORDER_ID, 0 );
995 WC()->session->set( WC_Vipps_Recurring_Helper::SESSION_ADDRESS_HASH, false );
996 }
997
998 if ( is_a( $order, 'WC_Order' ) && $order->get_status() === 'pending' ) {
999 // We want to kill orders that have failed, or that the user has abandoned. To do this,
1000 // we must ensure that no race or other mechanism kills the order while or after being paid.
1001 // if order is in the process of being finalized, don't kill it
1002 if ( WC_Vipps_Recurring_Helper::order_locked( $order ) ) {
1003 return false;
1004 }
1005
1006 // Get it again to ensure we have all the info, and check status again
1007 clean_post_cache( $order->get_id() );
1008 $order = wc_get_order( $order->get_id() );
1009 if ( $order->get_status() !== 'pending' ) {
1010 return false;
1011 }
1012
1013 // And to be extra sure, check status at Vipps/MobilePay
1014 $session = $order->get_meta( WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_SESSION );
1015 $poll_endpoint = ( $session && isset( $session['pollingUrl'] ) ) ? $session['pollingUrl'] : false;
1016
1017 if ( $poll_endpoint ) {
1018 try {
1019 WC_Vipps_Recurring_Logger::log( "Polling checkout from abandon_checkout_order" );
1020 $poll_data = $this->gateway()->api->checkout_poll( $poll_endpoint );
1021 $session_state = ( ! empty( $poll_data ) && is_array( $poll_data ) && isset( $poll_data['sessionState'] ) ) ? $poll_data['sessionState'] : "";
1022 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Checking Checkout status on cart/order change: %s", $order->get_id(), $session_state ) );
1023 if ( $session_state === 'PaymentSuccessful' || $session_state === 'PaymentInitiated' ) {
1024 // If we have started payment, we do not kill the order.
1025 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Checkout payment started - cannot cancel", $order->get_id() ) );
1026
1027 return false;
1028 }
1029 } catch ( Exception $e ) {
1030 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Could not get Checkout status for order. Order is still in progress while cancelling', $order->get_id() ) );
1031 }
1032 }
1033
1034 // NB: This can *potentially* be revived by a callback!
1035 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Cancelling Checkout order because order changed', $order->get_id() ) );
1036 $order->set_status( 'cancelled', __( "Order specification changed - order abandoned by customer in Checkout", 'woo-vipps' ), false );
1037
1038 // Also mark for deletion and remove stored session
1039 WC_Vipps_Recurring_Helper::delete_meta_data( $order, WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_SESSION );
1040
1041 // Stop checking the status of this order in cron
1042 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING, false );
1043
1044 // This is dealt with by a cron schedule
1045 if ( $this->gateway()->get_option( 'checkout_cleanup_abandoned_orders' ) === 'yes' ) {
1046 $subscriptions = wcs_get_subscriptions_for_order( $order->get_id() );
1047 foreach ( $subscriptions as $subscription ) {
1048 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_MARKED_FOR_DELETION, 1 );
1049 $subscription->save();
1050 }
1051 }
1052
1053 $order->save();
1054 }
1055 }
1056
1057 /**
1058 * @throws WC_Vipps_Recurring_Exception
1059 * @throws WC_Vipps_Recurring_Temporary_Exception
1060 * @throws WC_Vipps_Recurring_Config_Exception
1061 * @throws WC_Data_Exception
1062 */
1063 public function handle_callback( array $body, string $authorization_token ): void {
1064 WC_Vipps_Recurring_Logger::log( sprintf( "Handling Vipps/MobilePay Checkout callback with body: %s", json_encode( $body ) ) );
1065
1066 $args = WC_Vipps_Recurring_Helper::add_meta_query_to_args( [], WC_Vipps_Recurring_Helper::META_ORDER_CHECKOUT_SESSION_ID, '=', $body['sessionId'], $this->gateway()->use_high_performance_order_storage() );
1067
1068 $orders = wc_get_orders( $args );
1069
1070 if ( empty( $orders ) ) {
1071 WC_Vipps_Recurring_Logger::log( sprintf( "Found no order ids in Vipps/MobilePay Checkout callback for session id: %s", $body['sessionId'] ) );
1072
1073 return;
1074 }
1075
1076 $order = array_pop( $orders );
1077
1078 $stored_authorization_token = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_ORDER_EXPRESS_AUTH_TOKEN );
1079 if ( $authorization_token !== $stored_authorization_token ) {
1080 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Invalid authorization token for session id %s.", WC_Vipps_Recurring_Helper::get_id( $order ), $body['sessionId'] ) );
1081
1082 return;
1083 }
1084
1085 $this->handle_payment( $order, $body );
1086 }
1087
1088 /**
1089 * @param WC_Order $order
1090 * @param array $session
1091 *
1092 * @return void
1093 * @throws WC_Data_Exception
1094 * @throws WC_Vipps_Recurring_Config_Exception
1095 * @throws WC_Vipps_Recurring_Exception
1096 * @throws WC_Vipps_Recurring_Temporary_Exception
1097 * @throws Exception
1098 */
1099 public function handle_payment( WC_Order $order, array $session ): void {
1100 $order_id = WC_Vipps_Recurring_Helper::get_id( $order );
1101
1102 if ( empty( $session['subscriptionDetails']['agreementId'] ) ) {
1103 return;
1104 }
1105
1106 $agreement_id = $session['subscriptionDetails']['agreementId'];
1107 $status = $session['sessionState'];
1108
1109 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Handling Vipps/MobilePay Checkout payment for agreement ID %s with status %s", $order_id, $agreement_id, $status ) );
1110
1111 // This makes sure we are covered by all our normal cron checks as well
1112 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, $agreement_id );
1113
1114 $order_charge_id = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_CHARGE_ID );
1115 if ( empty( $order_charge_id ) ) {
1116 $charges = $this->gateway()->api->get_charges_for( $agreement_id );
1117 /** @var WC_Vipps_Charge $charge */
1118 $charge = array_pop( $charges );
1119
1120 WC_Vipps_Recurring_Helper::update_meta_data( $order, WC_Vipps_Recurring_Helper::META_CHARGE_ID, $charge->id );
1121 }
1122
1123 $order->save();
1124
1125 // "SessionCreated" "PaymentInitiated" "SessionExpired" "PaymentSuccessful" "PaymentTerminated"
1126 if ( in_array( $status, [ 'SessionExpired', 'PaymentTerminated' ] ) ) {
1127 $this->abandon_checkout_order( $order );
1128
1129 return;
1130 }
1131
1132 if ( $status !== 'PaymentSuccessful' ) {
1133 return;
1134 }
1135
1136 // On success, we might have to create a user as well, if they don't already exist, this is because Woo Subscriptions REQUIRE a user.
1137 $email = sanitize_email( $session['billingDetails']['email'] ?? '' );
1138 $customer_id = (int) $order->get_customer_id( 'edit' );
1139 $anonymous_customer_id = (int) get_option( WC_Vipps_Recurring_Helper::OPTION_ANONYMOUS_SYSTEM_CUSTOMER_ID );
1140 $user = $customer_id ? get_user_by( 'ID', $customer_id ) : false;
1141 $has_real_user = $user && $customer_id !== $anonymous_customer_id;
1142 if ( ! $has_real_user && ! $email ) {
1143 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Unable to assign the Checkout order because the billing email is empty', $order_id ) );
1144
1145 return;
1146 }
1147
1148 if ( ! $has_real_user ) {
1149 $user = get_user_by( 'email', $email );
1150
1151 if ( $user ) {
1152 $has_real_user = true;
1153 $order->set_customer_id( $user->ID );
1154 $order->save();
1155 }
1156 }
1157
1158 if ( ! $has_real_user ) {
1159 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Handling Vipps/MobilePay Checkout payment: creating a new customer", $order_id ) );
1160
1161 $firstname = $session['billingDetails']['firstName'];
1162 $lastname = $session['billingDetails']['lastName'];
1163 $name = $firstname;
1164
1165 $userdata = [
1166 'user_nicename' => $name,
1167 'display_name' => "$firstname $lastname",
1168 'nickname' => $firstname,
1169 'first_name' => $firstname,
1170 'last_name' => $lastname
1171 ];
1172
1173 $username = apply_filters( 'woo_vipps_express_checkout_new_username', '', $email, $userdata, $order );
1174 $user_id = wc_create_new_customer( $email, $username, wp_generate_password(), $userdata );
1175 if ( is_wp_error( $user_id ) ) {
1176 $user = get_user_by( 'email', $email );
1177 if ( ! $user ) {
1178 WC_Vipps_Recurring_Logger::log( sprintf( '[%s] Unable to create or find the Checkout customer: %s', $order_id, $user_id->get_error_message() ) );
1179
1180 return;
1181 }
1182
1183 $user_id = $user->ID;
1184 }
1185
1186 $customer = new WC_Customer( $user_id );
1187 $this->maybe_update_billing_and_shipping( $customer, $session );
1188
1189 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Handling Vipps/MobilePay Checkout payment: replacing customer with new id %s", $order_id, $user_id ) );
1190
1191 $order->set_customer_id( $user_id );
1192 $order->save();
1193
1194 $customer = new WC_Customer( $user_id );
1195 do_action( 'woo_vipps_express_checkout_new_customer', $customer, $order->get_id() );
1196 }
1197
1198 // Refresh order
1199 $order = wc_get_order( $order_id );
1200
1201 $this->maybe_update_billing_and_shipping( $order, $session );
1202
1203 // Update subscription with the correct customer id, and agreement id
1204 $existing_subscriptions = wcs_get_subscriptions_for_order( $order );
1205
1206 // Create a subscription if we have no subscription, because it might've been deleted previously if this session has been recovered.
1207 if ( empty( $existing_subscriptions ) ) {
1208 $existing_subscriptions = $this->gateway()->create_partial_subscriptions_from_order( $order );
1209 }
1210
1211 // If we still have no subscriptions, something is wrong. Log it
1212 if ( empty( $existing_subscriptions ) ) {
1213 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Unable to create a subscription for Checkout order and agreement ID %s", $order_id, $agreement_id ) );
1214
1215 return;
1216 }
1217
1218 /** @var WC_Subscription $subscription */
1219 $subscription = array_pop( $existing_subscriptions );
1220
1221 if ( ! $subscription ) {
1222 return;
1223 }
1224
1225 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_AGREEMENT_ID, $agreement_id );
1226
1227 $subscription->set_customer_id( $order->get_customer_id( 'edit' ) );
1228 wcs_copy_order_address( $order, $subscription );
1229 $subscription->save();
1230
1231 $this->gateway()->check_charge_status( $order_id, true );
1232
1233 // This is passed off to regular cron or API handling after this point
1234 WC_Vipps_Recurring_Logger::log( sprintf( "[%s] Finished handling Vipps/MobilePay Checkout payment for agreement ID %s", $order_id, $agreement_id ) );
1235 }
1236
1237 public function maybe_update_billing_and_shipping( $object, $session ): void {
1238 $contact = $session['billingDetails'] ?? $session['shippingDetails'];
1239
1240 if ( empty( $contact ) ) {
1241 return;
1242 }
1243
1244 if ( ! trim( $object->get_billing_email() ) || trim( $object->get_billing_email() ) === trim( WC_Vipps_Recurring_Helper::FAKE_USER_EMAIL ) ) {
1245 $object->set_billing_email( $contact['email'] );
1246 }
1247
1248 $object->set_billing_phone( '+' . $contact['phoneNumber'] );
1249 $object->set_billing_first_name( $contact['firstName'] );
1250 $object->set_billing_last_name( $contact['lastName'] );
1251 $object->set_billing_address_1( $contact['streetAddress'] );
1252 $object->set_billing_city( $contact['city'] );
1253 $object->set_billing_postcode( $contact['postalCode'] );
1254 $object->set_billing_country( $contact['country'] );
1255
1256 if ( isset( $session['shippingDetails'] ) ) {
1257 $contact = $session['shippingDetails'];
1258 }
1259
1260 $object->set_shipping_first_name( $contact['firstName'] );
1261 $object->set_shipping_last_name( $contact['lastName'] );
1262 $object->set_shipping_address_1( $contact['streetAddress'] );
1263 $object->set_shipping_city( $contact['city'] );
1264 $object->set_shipping_postcode( $contact['postalCode'] );
1265 $object->set_shipping_country( $contact['country'] );
1266
1267 $object->save();
1268 }
1269
1270 public function maybe_cancel_initial_order( WC_Order $order ): void {
1271 $created = $order->get_date_created();
1272 $now = time();
1273
1274 try {
1275 $timestamp = $created->getTimestamp();
1276 } catch ( Exception $e ) {
1277 // PHP 8 gives ValueError for certain older versions of WooCommerce here.
1278 $timestamp = intval( $created->format( 'U' ) );
1279 }
1280
1281 $passed = $now - $timestamp;
1282 $minutes = ( $passed / 60 );
1283
1284 if ( $order->get_status() === 'pending' && $minutes > 120 ) {
1285 $this->abandon_checkout_order( $order );
1286 }
1287 }
1288
1289 public function user_has_subscription( $has_subscription, $user_id, $product_id, $status ) {
1290 $subscriptions = wcs_get_users_subscriptions( $user_id );
1291
1292 foreach ( $subscriptions as $subscription ) {
1293 if ( ! WC_Vipps_Recurring_Helper::get_meta( $subscription, WC_Vipps_Recurring_Helper::META_ORDER_IS_CHECKOUT ) ) {
1294 continue;
1295 }
1296
1297 // You do not have a subscription simply because you have a pending subscription.
1298 // Checkout subscriptions are created BEFORE a payment is made.
1299 if ( $subscription->has_status( 'pending' ) ) {
1300 $has_subscription = false;
1301 break;
1302 }
1303 }
1304
1305 return $has_subscription;
1306 }
1307 }
1308