PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.1.8
Pay with Vipps and MobilePay for WooCommerce v6.1.8
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 5.4.1 5.4.0 5.3.4 trunk All 183 releases
woo-vipps / payment / VippsCheckout.class.php

VippsCheckout.class.php in Pay with Vipps and MobilePay for WooCommerce 6.1.8, at payment/VippsCheckout.class.php

1,620 lines 82.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 This class is for hooks and plugin managent for the Vipps Checkout checkout-replacement feature, and is instantiated as a singleton.
4 IOK 2023-05-16
5 For WP-specific interactions.
6
7
8 This file is part of the plugin Pay with Vipps and MobilePay for WooCommerce
9 Copyright (c) 2023 WP-Hosting AS
10
11 MIT License
12
13 Copyright (c) 2023 WP-Hosting AS
14
15 Permission is hereby granted, free of charge, to any person obtaining a copy
16 of this software and associated documentation files (the "Software"), to deal
17 in the Software without restriction, including without limitation the rights
18 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19 copies of the Software, and to permit persons to whom the Software is
20 furnished to do so, subject to the following conditions:
21
22 The above copyright notice and this permission notice shall be included in all
23 copies or substantial portions of the Software.
24
25 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31 SOFTWARE.
32
33
34 */
35 if ( ! defined( 'ABSPATH' ) ) {
36 exit; // Exit if accessed directly
37 }
38
39 class VippsCheckout {
40 private static $instance = null;
41 private $gw = null;
42 private $payid = 0; // Used to improve handling of "what is the current checkout page" IOK 2024-11-14
43
44 public static function instance() {
45 if (!static::$instance) static::$instance = new VippsCheckout();
46 return static::$instance;
47 }
48
49 public function gateway() {
50 if ($this->gw) return $this->gw;
51 $this->gw = Vipps::instance()->gateway();
52 return $this->gw;
53 }
54
55 public static function register_hooks() {
56 $VippsCheckout = static::instance();
57 if (is_admin()) {
58 add_action('admin_init',array($VippsCheckout,'admin_init'));
59 }
60 add_action('init',array($VippsCheckout,'init'));
61 add_action( 'woocommerce_loaded', array($VippsCheckout,'woocommerce_loaded'));
62 add_action( 'template_redirect', array($VippsCheckout,'template_redirect'));
63 add_action( 'admin_post_nopriv_vipps_gw', array($VippsCheckout, 'choose_other_gw'));
64 add_action( 'admin_post_vipps_gw', array($VippsCheckout, 'choose_other_gw'));
65 add_filter( 'woocommerce_order_email_verification_required', array($VippsCheckout, 'allow_other_payment_method_email'), 10, 3);
66 add_action('wp_footer', array($VippsCheckout, 'maybe_proceed_to_payment'));
67
68 // Expire the Checkout session on order cancel. LP 2025-09-25
69 add_action('woocommerce_order_status_cancelled', array($VippsCheckout, 'woocommerce_order_status_cancelled'));
70
71
72 add_filter('woo_vipps_shipping_method_pickup_points', function ($points, $rate, $shipping_method, $order) {
73 if ($rate->method_id == 'pickup_location' && $order->get_meta('_vipps_checkout')) {
74 // $locations = $shipping_method->pickup_locations ; // Protected attribute. Could wrap, but life is short so
75 $locations = get_option( $shipping_method->id . '_pickup_locations', [] );
76 foreach ( $locations as $index => $location ) {
77 if ( ! $location['enabled'] ) {
78 continue;
79 }
80
81 $addr = $location['address'] ?? [];
82 $default_country = WC()->countries->get_base_country() ?: "NO";
83
84 $point = [];
85 $point['id'] = "$index";
86 $point['name'] = $location['name'] ?: " ";
87 $point['address'] = $addr['address_1'] ?: " ";
88 $point['postalCode'] = $addr['postcode'] ?: " ";
89 $point['city'] = $addr['city'] ?: " ";
90 $point['country'] = $addr['country'] ?: $default_country;
91 $points[] = $point;
92 }
93 }
94 return $points;
95 }, 10, 4);
96
97
98 }
99
100 function woocommerce_order_status_cancelled ($orderid) {
101 $order = wc_get_order($orderid);
102 // Do this only if we have a checkout saved in the order
103 $is_checkout = $order->get_meta('_vipps_checkout_session');
104 if (!$is_checkout) return;
105 try {
106 // If the order isn't in the right state, this will do nothing. IOK 2025-10-08
107 $this->gateway()->api->checkout_expire_session($order);
108 } catch (Exception $e) {
109 $this->log(sprintf(__("Cannot expire checkout session for order %d: %s", 'woo-vipps'), $orderid, $e->getMessage()));
110 }
111 // Cleanup the order, and ensure that we don't do this twice by deleting the old session
112 $order->delete_meta_data('_vipps_checkout_session');
113 $order->save_meta_data();
114 }
115
116 public function allow_other_payment_method_email ($email_verification_required, $order, $context ) {
117 if (is_checkout_pay_page()) {
118 $proceed = $order->get_meta('_vc_proceed');
119 if ($proceed) {
120 // Maybe do a timestamp check here FIXME
121 return false;
122 }
123 }
124 return $email_verification_required;
125 }
126
127 # For "Choose other payment method" in Vipps Checkout, we will decorate the order with the chosen gw
128 # and use javascript to go directly to that method. This is partly because we can't set default gateway to
129 # kco on the order pay screen. IOK 2024-05-15
130 public function maybe_proceed_to_payment() {
131 if (is_checkout_pay_page()) {
132 $orderid = absint(get_query_var( 'order-pay'));
133 $order = $orderid ? wc_get_order($orderid) : null;
134 $gateway = sanitize_title(trim(is_a($order, 'WC_Order') ? $order->get_meta('_vc_proceed') : false));
135 $method = $gateway;
136 // Choose invoice payment if Klarna payments is the gateway. IOK 2024-10-11
137 if ($gateway == 'klarna_payments') {
138 $method = 'klarna_payments_pay_later';
139 }
140 $method = apply_filters('woo_vipps_checkout_external_payment_method_selected', $method, $gateway, $order);
141 $order->update_meta_data('_vc_proceed', 'any'); // Just in case we return to the order-pay page
142 $order->save();
143
144 if ($method != 'any'):
145 ?>
146 <script>
147 jQuery(document).ready(function () {
148 let selected = jQuery('input#payment_method_<?php echo sanitize_title($method); ?>[name="payment_method"]');
149 if (selected.length > 0) {
150 jQuery('input#terms[type="checkbox"]').prop('checked', true).trigger('change');
151 selected.prop('checked', true).trigger('change');
152 // Neccessary for Klarna Payments
153 setTimeout(function () {
154 jQuery('button#place_order').click(); }, 100);
155 }
156 });
157 </script>
158 <?php
159 endif;
160 }
161 }
162
163 // Returns list of external payment methods - from Vipps id to gateway id.
164 // IOK 2024-10-11 filterable by 'woo_vipps_checkout_external_payment_methods'
165 public function external_payment_methods() {
166 $available = array_keys(WC()->payment_gateways->get_available_payment_gateways());
167 $gw = WC_Gateway_Vipps::instance();
168 $ok = $gw->allow_external_payments_in_checkout();
169 if (!$ok) return [];
170 // Only defined value at this point - klarna means either of these gateways (IOK 2024-05-28)
171 // Prioritize payments if present
172 $possible = ['klarna' => ['klarna_payments', 'kco']];
173 $externals = [];
174 foreach ($possible as $key => $gws) {
175 $on = $gw->get_option('checkout_external_payments_' . $key);
176 $active = array_intersect($gws, $available);
177 if ($on == "yes" && !empty($active)) {
178 $externals[$key] = ['gw' => array_values($active)[0]];
179 }
180 }
181 return $externals;
182 }
183
184 # Called in admin-post and will finalize a Vipps Checkout order + send the customer to the payment page.
185 public function choose_other_gw () {
186 $orderid = intval($_GET['o']);
187 $gw = trim(sanitize_title($_GET['gw']));
188 if ($gw == 'any') $gw = "";
189 $nonce = $_GET['cb'];
190 $ok = wp_verify_nonce($nonce, 'vipps_gw');
191 if (!$ok) {
192 $this->abandonVippsCheckoutOrder(false);
193 $this->log(sprintf(__("Orderid %1\$s: Wrong nonce when trying to switch payment methods.", 'woo-vipps'), $orderid), 'error');
194 wp_redirect(home_url());
195 }
196 $order = wc_get_order($orderid);
197 if (!$order || $order->get_status() != 'pending') {
198 $this->abandonVippsCheckoutOrder(false);
199 $this->log(sprintf(__("Orderid %1\$s is not pending when choosing another payment method from Vipps Checkout", 'woo-vipps'), $orderid), 'error');
200 wp_redirect(home_url());
201 }
202
203 try {
204 // Load session from cookies - it will not get loaded on admin-post.
205 WC()->initialize_session();
206
207 if (WC()->session) {
208 if (! WC()->session->has_session()) {
209 WC()->session->set_customer_session_cookie( true );
210 }
211 // There is actually a bug here for KCO which will redirect to the normal checkout page with an error message.
212 // Try to stop that.. IOK 2024-05-15
213 if ($gw != 'kco') {
214 WC()->session->set('chosen_payment_method', $gw);
215 }
216 $addressdata = [];
217 $addressdata["billing_email"] = $order->get_billing_email();
218 $addressdata["billing_address_1"] = $order->get_billing_address_1();
219 $addressdata["billing_address_2"] = $order->get_billing_address_2();
220 $addressdata["billing_postcode"] = $order->get_billing_postcode();
221 $addressdata["billing_city"] = $order->get_billing_city();
222 $addressdata["billing_country"] = $order->get_billing_country();
223 $addressdata["billing_first_name"] = $order->get_billing_first_name();
224 $addressdata["billing_last_name"] = $order->get_billing_last_name();
225 $addressdata["billing_phone"] = $order->get_billing_phone();
226 $addressdata["billing_city"] = $order->get_billing_city();
227 $addressdata["shipping_address_1"] = $order->get_shipping_address_1();
228 $addressdata["shipping_address_2"] = $order->get_shipping_address_2();
229 $addressdata["shipping_city"] = $order->get_shipping_city();
230 $addressdata["shipping_postcode"] = $order->get_shipping_postcode();
231 $addressdata["shipping_country"] = $order->get_shipping_country();
232 $addressdata["shipping_first_name"] = $order->get_shipping_first_name();
233 $addressdata["shipping_last_name"] = $order->get_shipping_last_name();
234 $addressdata["shipping_phone"] = $order->get_shipping_phone();
235 $addressdata["shipping_city"] = $order->get_shipping_city();
236 WC()->session->set('vc_address', $addressdata);
237 WC()->session->save_data();
238 } else {
239 $this->log(__("No session choosing other gateway from Vipps Checkout", 'woo-vipps'), 'error');
240 }
241
242 $current_pending = is_a(WC()->session, 'WC_Session') ? WC()->session->get('vipps_checkout_current_pending') : false;
243
244 // If we got here, we actually have shipping information already in place, so we can continue with the order directly!
245 $paymentdetails = WC_Gateway_Vipps::instance()->get_payment_details($order);
246 // As of writing, userDetails is not included in the return. LP 2026-03-13
247 $paymentdetails = WC_Gateway_Vipps::instance()->ensure_userDetails($paymentdetails, $order);
248
249 // Unless the order actually somehow doesn't exist anymore - this should not be possible but let's handle it
250 if ($paymentdetails && isset($paymentdetails['status']) && $paymentdetails['status'] == 'CANCEL') {
251 // IOK this cannot actually happen, but if it *does*, cancel the order
252 clean_post_cache($order->get_id());
253 if (WC()->session) {
254 WC()->session->set('vipps_checkout_current_pending',0);
255 WC()->session->set('vipps_address_hash', false);
256 }
257 $order->set_status('cancelled', __("Session terminated with no payment", 'woo-vipps'), false);
258 // Also mark for deletion and remove stored session
259 $order->update_meta_data('_vipps_delendum',1);
260 $order->save();
261 $url = $order->get_checkout_payment_url();
262 wp_redirect($url);
263 exit();
264 }
265
266 $billing = isset($paymentdetails['billingDetails']) ? $paymentdetails['billingDetails'] : false;
267 # Don't assign the order to its user if we are not logged in - we are not completing this order using Vipps IOK 2024-05-15
268 $assignuser = is_user_logged_in();
269
270 WC_Gateway_Vipps::instance()->set_order_shipping_details($order,($paymentdetails['shippingDetails'] ?? []), $paymentdetails['userDetails'], $billing, $paymentdetails, $assignuser);
271
272 # Now reset payment gateway and clear out the VC session
273 $order->set_payment_method($gw);
274 $order->update_meta_data('_vc_proceed', ($gw ? $gw : 'any'));
275 $order->add_order_note(sprintf(__('Alternative payment method "%1$s" chosen, customer returned from Checkout', 'woo-vipps'), $gw));
276 $order->save();
277 // Should not be neccessary but we'll add this just so any caching does not cause other systems to return the wrong data here IOK 2025-03-18
278 clean_post_cache($order->get_id());
279
280 $url = $order->get_checkout_payment_url();
281
282 // This makes sure there is no "current vipps checkout" order, as this order is no longer payable at Vipps.
283 // Actually, don't do this because it will most likely just create more spurious orders while this remains unpayable. IOK 2024-06-04
284 // $this->abandonVippsCheckoutOrder(false);
285 wp_redirect($url);
286 exit();
287 } catch (Exception $e) {
288 $msg = sprintf(__("Could not select other payment gateway for order %1\$s", 'woo-vipps'), $order->get_id());
289 $this->log($msg, 'error');
290 $order->set_status('cancelled', $msg, false);
291 // Also mark for deletion and remove stored session
292 $order->update_meta_data('_vipps_delendum',1);
293 $order->save();
294 $this->abandonVippsCheckoutOrder(false);
295 $url = $order->get_checkout_payment_url();
296 wp_redirect($url);
297 exit();
298 }
299 }
300
301 public function template_redirect () {
302 // This will normally be on the "checkout" page which shouldn't be cached, but just in case, add
303 // nocache headres to any page that uses this shortcode. IOK 2021-08-26
304 // Furthermore, sometimes woocommerce calls is_checkout() *before* woocommerce is loaded, so
305 global $post;
306
307 // Modify cart coupon validation - has to be very early because otherwise we can't show
308 // notices in gutenberg cart IOK 2025-05-15
309 $this->handle_coupon_invalidation_in_checkout();
310
311 if ($post && is_page() && has_shortcode($post->post_content, 'vipps_checkout')) {
312 // Add fonts for the widgets on this page IOK 2025-05-02
313 wp_enqueue_style('vipps-fonts',plugins_url('css/fonts.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/fonts.css"), 'all');
314
315 add_filter('woocommerce_is_checkout', '__return_true');
316 add_filter('body_class', function ($classes) {
317 $classes[] = 'vipps-checkout';
318 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
319 return apply_filters('woo_vipps_checkout_body_class', $classes);
320 });
321 /* Suppress the title for this page, but on the front page only IOK 2023-01-27 (by request from Vipps) */
322 $post_to_hide_title_for = $post->ID;
323 add_filter('the_title', function ($title, $postid = 0) use ($post_to_hide_title_for) {
324 if (!is_admin() && $postid == $post_to_hide_title_for && is_singular() && in_the_loop()) {
325 $title = "";
326 }
327 return $title;
328 }, 10, 2);
329 Vipps::nocache();
330 }
331 }
332
333
334 public function init () {
335 add_action('wp_loaded', array($this, 'wp_register_scripts'));
336
337 // For Vipps Checkout, poll for the result of the current session
338 add_action('wp_ajax_vipps_checkout_poll_session', array($this, 'vipps_ajax_checkout_poll_session'));
339 add_action('wp_ajax_nopriv_vipps_checkout_poll_session', array($this, 'vipps_ajax_checkout_poll_session'));
340 // Use ajax to initiate the session too
341 add_action('wp_ajax_vipps_checkout_start_session', array($this, 'vipps_ajax_checkout_start_session'));
342 add_action('wp_ajax_nopriv_vipps_checkout_start_session', array($this, 'vipps_ajax_checkout_start_session'));
343
344 // And for user-defined callbacks in the widgets. These may modify the order.
345 add_action('wp_ajax_vipps_checkout_callback', array($this, 'vipps_ajax_checkout_callback'));
346 add_action('wp_ajax_nopriv_vipps_checkout_callback', array($this, 'vipps_ajax_checkout_callback'));
347
348 // Check cart total before initiating Vipps Checkout NT-2024-09-07
349 // This allows for real-time validation of the cart before proceeding with the checkout process
350 add_action('wp_ajax_vipps_checkout_validate_cart', array($this, 'ajax_vipps_checkout_validate_cart'));
351 add_action('wp_ajax_nopriv_vipps_checkout_validate_cart', array($this, 'ajax_vipps_checkout_validate_cart'));
352
353 // Retrieve widgets - this is done by ajax so as to ensure that the Order object exists at this point.
354 add_action('wp_ajax_vipps_checkout_get_widgets', array($this, 'vipps_ajax_get_widgets'));
355 add_action('wp_ajax_nopriv_vipps_checkout_get_widgets', array($this, 'vipps_ajax_get_widgets'));
356
357 // Prevent previews and prefetches of the Vipps Checkout page starting and creating orders
358 add_action('wp_head', array($this, 'wp_head'));
359
360
361 // The Vipps Checkout feature which overrides the normal checkout process uses a shortcode
362 add_shortcode('vipps_checkout', array($this, 'vipps_checkout_shortcode'));
363
364 // Ensure we remove the current session on the thank you page (too).
365 add_action('woocommerce_thankyou_vipps', function () {
366 WC()->session->set('vipps_checkout_current_pending',false);
367 WC()->session->set('vipps_address_hash', false);
368 WC()->session->set('vipps_shipping_rate_id_map', false);
369 });
370
371 // For Vipps Checkout - we need to know any time and as soon as the cart changes, so fold all the events into a single one. IOK 2021-08-24
372 add_action( 'woocommerce_add_to_cart', function ($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
373 do_action('vipps_cart_changed');
374 }, 10, 6);
375 // Cart emptied
376 add_action( 'woocommerce_cart_emptied', function ($clear_persistent_cart) {
377 do_action('vipps_cart_changed');
378 }, 10, 1);
379 // After updating quantities
380 add_action('woocommerce_after_cart_item_quantity_update', function ( $cart_item_key, $quantity, $old_quantity ) {
381 do_action('vipps_cart_changed');
382 }, 10, 3);
383 // Blocks and ajax
384 add_action( 'woocommerce_cart_item_removed', function ($cart_item_key, $cart) {
385 do_action('vipps_cart_changed');
386 }, 10, 3);
387 // Restore deleted entry
388 add_action( 'woocommerce_cart_item_restored', function ($cart_item_key, $cart) {
389 do_action('vipps_cart_changed');
390 }, 10, 3);
391 // Normal cart form update
392 add_filter('woocommerce_update_cart_action_cart_updated', function ($updated) {
393 do_action('vipps_cart_changed');
394 return $updated;
395 });
396 // Trigger cart_changed when a coupon is applied
397 add_action('woocommerce_applied_coupon', array($this, 'cart_changed'));
398 // Trigger cart_changed when a coupon is removed
399 add_action('woocommerce_removed_coupon', array($this, 'cart_changed'));
400 // Then handle the actual cart change
401 add_action('vipps_cart_changed', array($this, 'cart_changed'));
402 }
403
404 public function admin_init () {
405 // Stuff for the special Vipps Checkout page
406 add_filter('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'), 10, 1);
407 }
408
409 // Extra stuff in the <head>, which will mostly mean dynamic CSS
410 public function wp_head () {
411 // If we have a Vipps Checkout page, stop iOS from giving previews of it that
412 // starts the session - iOS should use the visibility API of the browser for this, but it doesn't as of 2021-11-11
413 $checkoutid = wc_get_page_id('vipps_checkout');
414 if ($checkoutid) {
415 $url = get_permalink($checkoutid);
416 echo "<style> a[href=\"$url\"] { -webkit-touch-callout: none; } </style>\n";
417 }
418 }
419
420 public function wp_register_scripts () {
421 $sdkurl = 'https://checkout.vipps.no/vippsCheckoutSDK.js';
422 wp_register_script('vipps-sdk',$sdkurl,array());
423 wp_register_script('vipps-checkout-widgets', plugins_url('js/vipps-checkout-widgets.js', __FILE__), [], filemtime(dirname(__FILE__) . "/js/vipps-checkout-widgets.js"), 'true');
424 wp_register_script('vipps-checkout',plugins_url('js/vipps-checkout.js',__FILE__),array('vipps-gw','vipps-sdk', 'vipps-checkout-widgets'),filemtime(dirname(__FILE__) . "/js/vipps-checkout.js"), 'true');
425 }
426
427 public function log ($what,$type='info') {
428 $logger = function_exists('wc_get_logger') ? wc_get_logger() : false;
429 if ($logger) {
430 $context = array('source'=>'woo-vipps');
431 $logger->log($type,$what,$context);
432 } else {
433 error_log("woo-vipps ($type): $what");
434 }
435 }
436
437
438 // This is used by the Vipps Checkout page to start the Vipps checkout session, including
439 // creating the partial order IOK 2021-09-03
440 // IOK FIXME: Add support for starting payment *for a given order*. 2023-08-15
441 public function vipps_ajax_checkout_start_session () {
442 check_ajax_referer('do_vipps_checkout','vipps_checkout_sec');
443 $url = "";
444 $redir = "";
445 $token = "";
446
447 Vipps::set_locale_if_in_header();
448
449 // First, check that we haven't already done this like in another window or something:
450 // IOK 2024-06-04 This also happens when using the back button! Sometimes!
451 $sessioninfo = $this->vipps_checkout_current_pending_session();
452
453 if (isset($sessioninfo['redirect'])) {
454 $redirect = $sessioninfo['redirect'];
455 }
456 if (isset($sessioninfo['session']) && isset($sessioninfo['session']['token'])) {
457 $token = $sessioninfo['session']['token'];
458 $src = $sessioninfo['session']['checkoutFrontendUrl'];
459 $url = $src;
460 }
461
462 // And if we do, just return what we have. NB: This *should not happen*.
463 // IOK 2025-05-04 what are you talking about IOK, this absolutely happens e.g. when using the backbutton to a page starting the orders.
464 if ($url || $redir) {
465 $current_pending = is_a(WC()->session, 'WC_Session') ? WC()->session->get('vipps_checkout_current_pending') : false;
466 return wp_send_json_success(array('ok'=>1, 'msg'=>'session started', 'src'=>$url, 'redirect'=>$redir, 'token'=>$token, 'orderid'=>$current_pending));
467 }
468
469 // Otherwise, create an order and start a new session
470 $session = null;
471 $current_pending = 0;
472 $current_authtoken = "";
473 $limited_session = "";
474 try {
475 $current_pending = $this->gateway()->create_partial_order('ischeckout');
476 if ($current_pending) {
477 $order = wc_get_order($current_pending);
478 $order->update_meta_data('_vipps_checkout', true);
479 $current_authtoken = $this->gateway()->generate_authtoken();
480 $limited_session = $this->gateway()->generate_authtoken();
481 $order->update_meta_data('_vipps_authtoken',wp_hash_password($current_authtoken));
482 $order->update_meta_data('_vipps_limited_session',wp_hash_password($limited_session));
483 $order->save();
484 WC()->session->set('vipps_checkout_current_pending', $current_pending);
485
486 try {
487 Vipps::instance()->maybe_add_static_shipping($this->gateway(),$order->get_id(), 'vippscheckout');
488 } catch (Exception $e) {
489 // In this case, we just have to continue.
490 $this->log(sprintf(__("Error calculating static shipping for order %1\$s", 'woo-vipps'), $order->get_id()), 'error');
491 $this->log($e->getMessage(),'error');
492 }
493 $this->gateway()->save_session_in_order($order);
494 do_action('woo_vipps_checkout_order_created', $order);
495 } else {
496 throw new Exception(sprintf(__('Unknown error creating %1$s partial order', 'woo-vipps'), Vipps::CheckoutName()));
497 }
498 } catch (Exception $e) {
499 return wp_send_json_success(array('ok'=>0, 'msg'=>$e->getMessage(), 'src'=>'', 'redirect'=>'', 'orderid'=>0));
500 }
501
502 // Ensure we get the latest updates to the order too IOK 2021-10-22
503 $order = wc_get_order($current_pending);
504 if (is_user_logged_in()) {
505 $phone = get_user_meta(get_current_user_id(), 'billing_phone', true);
506 }
507 $order_id = $order->get_id();
508 $returnurl = Vipps::instance()->payment_return_url();
509 $returnurl = add_query_arg('ls',$limited_session,$returnurl);
510 $returnurl = add_query_arg('id', $order_id, $returnurl);
511
512 $sessionorders= WC()->session->get('_vipps_session_orders');
513 if (!$sessionorders) $sessionorders = array();
514 $sessionorders[$order_id] = 1;
515 WC()->session->set('_vipps_pending_order',$order_id);
516 WC()->session->set('_vipps_session_orders',$sessionorders);
517
518 $customer_id = get_current_user_id();
519 if ($customer_id) {
520 $customer = new WC_Customer( $customer_id );
521 } else {
522 $customer = WC()->customer;
523 }
524
525 if ($customer) {
526 $customerinfo['email'] = $customer->get_billing_email();
527 $customerinfo['firstName'] = $customer->get_billing_first_name();
528 $customerinfo['lastName'] = $customer->get_billing_last_name();
529 $customerinfo['streetAddress'] = $customer->get_billing_address_1();
530 $address2 = trim($customer->get_billing_address_2());
531 if (!empty($address2)) {
532 $customerinfo['streetAddress'] = $customerinfo['streetAddress'] . ", " . $address2;
533 }
534 $customerinfo['city'] = $customer->get_billing_city();
535 $customerinfo['postalCode'] = $customer->get_billing_postcode();
536 $customerinfo['country'] = $customer->get_billing_country();
537
538 // Currently Vipps requires all phone numbers to have area codes and NOT the +-sign prefix, nor 00.
539 // We can't guaratee that at all, but try for Norway
540 $customerinfo['phoneNumber'] = "";
541 $phonenr = Vipps::normalizePhoneNumber($customer->get_billing_phone(), $customerinfo['country']);
542 if ($phonenr) {
543 $customerinfo['phoneNumber'] = $phonenr;
544 }
545 }
546
547 $customerinfo = apply_filters('woo_vipps_customerinfo', $customerinfo, $order);
548
549 try {
550 $session = $this->gateway()->api->initiate_checkout($customerinfo,$order,$returnurl,$current_authtoken);
551 if ($session) {
552 $order = wc_get_order($current_pending);
553 $order->update_meta_data('_vipps_init_timestamp',time());
554 $order->update_meta_data('_vipps_status','INITIATE');
555 $order->update_meta_data('_vipps_checkout_session', $session);
556
557 $order->add_order_note(sprintf(__('%1$s payment initiated','woo-vipps'), Vipps::CheckoutName()));
558 $order->add_order_note(sprintf(__('Customer passed to %1$s','woo-vipps'), Vipps::CheckoutName()));
559 $order->save();
560 $token = $session['token'];
561 $src = $session['checkoutFrontendUrl'];
562 $url = $src;
563 } else {
564 throw new Exception(sprintf(__('Unknown error creating %1$s session', 'woo-vipps'), Vipps::CheckoutName()));
565 }
566 } catch (Exception $e) {
567 $this->log(sprintf(__("Could not initiate %1\$s session: %2\$s", 'woo-vipps'), Vipps::CheckoutName(), $e->getMessage()), 'ERROR');
568 return wp_send_json_success(array('ok'=>0, 'msg'=>$e->getMessage(), 'src'=>'', 'redirect'=>'', 'orderid'=>$order_id));
569 }
570 if ($url || $redir) {
571 return wp_send_json_success(array('ok'=>1, 'msg'=>'session started', 'src'=>$url, 'redirect'=>$redir, 'token'=>$token, 'orderid'=>$order_id));
572 } else {
573 return wp_send_json_success(array('ok'=>0, 'msg'=>sprintf(__('Could not start %1$s session'), Vipps::CheckoutName()),'src'=>$url, 'redirect'=>$redir, 'orderid'=>$order_id));
574 }
575 }
576
577 // Handler function for all other callbacks from the Vipps MobilePay checkout screen - adding
578 // coupons, modifying the order etc. Actions are added with the filter 'woo_vipps_checkout_callback_actions' IOK 2025-05-13
579 // -- they are functions taking the action name and an order object. IOK 2025-05-13
580 public function vipps_ajax_checkout_callback() {
581 check_ajax_referer('do_vipps_checkout','vipps_checkout_sec');
582 $orderid = intval($_REQUEST['orderid']??0); // Currently not used because we are using a single pending order in session
583 $lock_held = intval($_REQUEST['lock_held'] ?? 0);
584 $action = sanitize_title($_REQUEST['callback_action'] ?? 0);
585
586 Vipps::set_locale_if_in_header();
587
588 add_filter('woo_vipps_is_checkout_callback', '__return_true'); // Signal that this is a special context.
589
590 // add some default action handlers IOK 2025-05-13
591 $this->add_widget_callback_actions();
592
593 $actions = apply_filters('woo_vipps_checkout_callback_actions', []);
594
595 $handler = $actions[$action] ?? false;
596 if (!$handler) {
597 $msg = sprintf(__("Vipps MobilePay Checkout callback with unknown action: %s", 'woo-vipps'), $action);
598 $this->log($msg, 'DEBUG');
599 return wp_send_json_error(array('msg'=>'FAILED', 'error'=>$msg));
600 }
601 // The single current pending order. IOK 2025-04-25
602 $current_pending = is_a(WC()->session, 'WC_Session') ? WC()->session->get('vipps_checkout_current_pending') : false;
603 $order = $current_pending ? wc_get_order($current_pending) : null;
604 if (!$order) {
605 return wp_send_json_error(array('msg'=>'FAILED', 'error'=>'Unknown order'));
606 }
607 $prevtotal = $order->get_total() ?: 0;
608 try {
609 $result = $handler($action, $order);
610 $order = wc_get_order($order->get_id()); // Incase the order has changed since last wc_get_order. LP 2025-05-14
611 $newtotal = $order->get_total() ?: 0;
612
613 if ($lock_held) {
614 // If the order total has changed, we may want to recalculate shipping methods.
615 list($new_shipping, $old_table) = $this->maybe_recalc_shipping_methods($order, $prevtotal, $newtotal);
616 if ($new_shipping || $newtotal != $prevtotal) {
617 try {
618 $res = $this->gateway()->api->checkout_modify_session($order, $new_shipping);
619 } catch (Exception $e) {
620 $this->log(__("Problem modifying Checkout session: ", 'woo-vipps') . $e->getMessage());
621 if ($new_shipping) {
622 $order->update_meta_data('_vipps_express_checkout_shipping_method_table', $old_table);
623 $order->save_meta_data();
624 }
625 }
626 }
627 }
628 return wp_send_json_success(array('msg'=>$result));
629 } catch (Exception $e) {
630 return wp_send_json_error(array('msg'=>'FAILED', 'error'=>$e->getMessage()));
631 }
632 }
633
634 // We call this in the *modify* branch of Vipps Checkout if the customer adds a coupon or changes the value
635 // of the order so that free shipping may be added or removed. IOK 2025-09-16
636 // On successful return, we will return both the new set of shipping rates and the old table so any errors can be reverted.
637 private function maybe_recalc_shipping_methods ($order, $old_price, $new_price) {
638 // We will only recalculate if we already have a shipping table; and because the "modify" call may fail,
639 // we'll return the previous table so that we can then revert to the previous table. IOK 2025-09-16
640 $existing_table = $order->get_meta('_vipps_express_checkout_shipping_method_table');
641 if (empty($existing_table)) return [false, false];
642
643 // This is any shipping method with zero cost which is *not* local pickup. We are not going to try to find "free shipping" rates or methods
644 // using load_shipping_methods or anything like that, because any shipping method can implement free shipping and we can't know which
645 // until we recalculate shipping for the package, which could be costly. Instead we'll look at the price of the order and any coupons that have been added.
646 // IOK 2025-09-16
647 $had_free_shipping = ($existing_table['_meta_has_free_shipping'] ?? false);
648
649 // We *should* recalc primarily if we did have free shipping, but the new price of the order is lower than the old one,
650 // or if we did *not* have free shipping but the new price is *larger*. This is because free shipping is typically linked to
651 // the cart value. IOK 2025-09-16
652 $should_recalc = ($had_free_shipping && ($old_price > $new_price)) || (!$had_free_shipping && ($old_price < $new_price));
653 // Then we run a filter on this so that forexample adding or removing a free-shipping coupont will have the neccessary effect. IOK 2025-09-16
654 $should_recalc = apply_filters('woo_vipps_checkout_recalculate_shipping', $should_recalc, $had_free_shipping, $order, $old_price);
655
656 if (!$should_recalc) return [false, false];
657
658 $vippsorderid = $order->get_meta('_vipps_orderid');
659 $new_return = Vipps::instance()->vipps_shipping_details_callback_handler($order, [],$vippsorderid, 'ischeckout');
660 $new_return = $new_return['shippingDetails'];
661
662 return [$new_return, $existing_table];
663 }
664
665 // Check the current status of the current Checkout session for the user.
666 public function vipps_ajax_checkout_poll_session () {
667 check_ajax_referer('do_vipps_checkout','vipps_checkout_sec');
668
669 $orderid = intval($_REQUEST['orderid']??0); // Currently not used because we are using a single pending order in session
670 $lock_held = intval($_REQUEST['lock_held'] ?? 0);
671 $type = $_REQUEST['type'] ?? "unknown"; // Type of callback
672 $polldata = $_REQUEST['pollData'] ?? []; // the actual Checkout js event data. LP 2026-03-19
673
674 Vipps::set_locale_if_in_header();
675
676 // The single current pending order. IOK 2025-04-25
677 $current_pending = is_a(WC()->session, 'WC_Session') ? WC()->session->get('vipps_checkout_current_pending') : false;
678 $order = $current_pending ? wc_get_order($current_pending) : null;
679
680 $payment_status = $order ? $this->gateway()->check_payment_status($order) : 'unknown';
681 if (in_array($payment_status, ['authorized', 'complete'])) {
682 $this->abandonVippsCheckoutOrder(false);
683 return wp_send_json_success(array('msg'=>'completed', 'url' => $this->gateway()->get_return_url($order)));;
684 }
685 if ($payment_status == 'cancelled') {
686 $this->log(sprintf(__("%1\$s session %2\$d cancelled (payment status)", 'woo-vipps'), Vipps::CheckoutName(), $order->get_id()), 'debug');
687 $this->abandonVippsCheckoutOrder($order);
688 return wp_send_json_error(array('msg'=>'FAILED', 'url'=>home_url()));
689 }
690
691 $session = $order ? $order->get_meta('_vipps_checkout_session') : false;
692 if (!$session) {
693 WC()->session->set('vipps_address_hash', false);
694 return wp_send_json_success(array('msg'=>'EXPIRED', 'url'=>false));
695 }
696
697 add_filter('woo_vipps_is_vipps_checkout', '__return_true');
698 $status = $this->get_vipps_checkout_status($order);
699
700 $failed = $status == 'ERROR' || $status == 'EXPIRED' || $status == 'TERMINATED';
701
702 // Disallow sessions that go on for too long.
703 if (is_a($order, "WC_Order")) {
704 $created = $order->get_date_created();
705 $timestamp = 0;
706 $now = time();
707 try {
708 $timestamp = $created->getTimestamp();
709 } catch (Exception $e) {
710 // PHP 8 gives ValueError for certain older versions of WooCommerce here.
711 $timestamp = intval($created->format('U'));
712
713 }
714 $passed = $now - $timestamp;
715 $minutes = ($passed / 60);
716 // Expire after 50 minutes
717 if ($minutes > 50) {
718 $this->log(sprintf(__("%1\$s session %2\$d expired after %3\$d minutes (limit 50)", 'woo-vipps'), Vipps::CheckoutName(), $order->get_id(), $minutes), 'debug');
719 $this->abandonVippsCheckoutOrder($order);
720 return wp_send_json_success(array('msg'=>'EXPIRED', 'url'=>false));
721 }
722 }
723
724 $ok = !$failed;
725
726 // Since we checked the payment status at Vipps directly above, we don't actaully have any extra information at this point.
727 // We do know that the session is live and ongoing, but that's it.
728
729 if ($failed) {
730 $msg = $status;
731 $this->log(sprintf(__("%1\$s session %2\$d failed with message %3\$s", 'woo-vipps'), Vipps::CheckoutName(), $order->get_id(), $msg), 'debug');
732 $this->abandonVippsCheckoutOrder($order);
733 return wp_send_json_error(array('msg'=>$msg, 'url'=>home_url()));
734 exit();
735 }
736 // Errorhandling! If this happens we have an unknown status or something like it.
737 if (!$ok) {
738 $this->log("Unknown status on polling status: " . print_r($status, true), 'ERROR');
739 $this->abandonVippsCheckoutOrder($order);
740 return wp_send_json_error(array('msg'=>'ERROR', 'url'=>false));
741 exit();
742 }
743
744 // This handles address information data from the poll if present. It is not, currently. 2021-09-27 IOK
745 // it is now! IOK 2024-04-24
746 $change = false;
747 $vipps_address_hash = WC()->session->get('vipps_address_hash');
748 if ($ok && (isset($status['billingDetails']) || isset($status['shippingDetails']))) {
749 $serialized = sha1(json_encode(@$status['billingDetails']) . ':' . json_encode(@$status['shippingDetails']));
750 if ($serialized != $vipps_address_hash) {
751 $change = true;
752 WC()->session->set('vipps_address_hash', $serialized);
753 }
754 }
755
756 // IOK This is the actual status of the order when this is called, which will
757 // include personalia only when available
758 if ($ok && $change && isset($status['billingDetails'])) {
759 $contact = $status['billingDetails'];
760 $order->set_billing_email($contact['email']);
761 $order->set_billing_phone($contact['phoneNumber']);
762 $order->set_billing_first_name($contact['firstName']);
763 $order->set_billing_last_name($contact['lastName']);
764 $order->set_billing_address_1($contact['streetAddress'] ?? "");
765 $order->set_billing_city($contact['city'] ?? "");
766 $order->set_billing_postcode($contact['postalCode'] ?? "");
767 $order->set_billing_country($contact['country'] ?? "");
768 }
769 if ($ok && $change && isset($status['shippingDetails'])) {
770 $contact = $status['shippingDetails'];
771 if ($contact['country'] ?? false) {
772 $countrycode = Vipps::instance()->country_to_code($contact['country']); // No longer neccessary IOK 2023-01-09
773 $order->set_shipping_first_name($contact['firstName']);
774 $order->set_shipping_last_name($contact['lastName']);
775 $order->set_shipping_address_1($contact['streetAddress']);
776 $order->set_shipping_city($contact['city']);
777 $order->set_shipping_postcode($contact['postalCode']);
778 $order->set_shipping_country($contact['country']);
779 }
780
781 }
782 if ($change) {
783 $order->save();
784 }
785
786 // When the address changes, the VAT/taxes may have changed too. Recalculate the order total if we know the Vipps lock
787 // of the order is held. IOK 2025-04-25
788 if ($change) {
789 $prevtotal = $order->get_total() ?: 0;
790 $newtotal = $order->calculate_totals(true); // With taxes please
791 if ($lock_held && $newtotal != $prevtotal) {
792 try {
793 $res = $this->gateway()->api->checkout_modify_session($order);
794 $order->save();
795 } catch (Exception $e) {
796 $this->log(__("Problem modifying Checkout session: ", 'woo-vipps') . $e->getMessage());
797 if ($newtotal < $prevtotal) {
798 // In this case, the orders value will be lower than what is reserved at Vipps, which is OK - the rest will be cancelled
799 // on order completion. IOK 2025-05-24
800 $order->save();
801 }
802 }
803 }
804 }
805
806 switch ($type) {
807 // Store the newly selected shipping method in session. LP 2026-03-19
808 // NB: selecting a new pickup location option inside the same pickup location group does NOT trigger an event here. LP 2026-03-19
809 case 'shipping_selected':
810 if (!is_a(WC()->session, 'WC_Session') || !is_a($order, 'WC_Order')) {
811 /* translators: order id */
812 $this->gw->log(sprintf(__('Checkout ajax new shipping: session not ok for order %1$s.', 'woo-vipps'), $order->get_id()), 'error');
813 break;
814 }
815
816 $new_shipping_key = $polldata['id'] ?? null; // our custom key like '$flat_rate$a1vkbszd221:index' used in shipping table, NOT the same as woo rate_id. LP 2026-03-19
817 if (!$new_shipping_key) {
818 break;
819 }
820
821 // Retrieve the woo rate id for new shipping selection, we have stored this in the session. LP 2026-03-20
822 $shipping_rate_id_map = WC()->session->get('vipps_shipping_rate_id_map');
823 $new_shipping_rate_id = $shipping_rate_id_map[$new_shipping_key] ?? null;
824
825 if (!$new_shipping_rate_id) {
826 /* translators: order id, shipping rate key */
827 $this->gw->log(sprintf(__('Checkout ajax new shipping: did not find shipping rate id for the new shipping choice "%2$s" order %1$s.', 'woo-vipps'), $order->get_id(), $new_shipping_key), 'error');
828 break;
829 }
830
831 $change = true;
832 WC()->session->set('chosen_shipping_methods', [$new_shipping_rate_id]);
833 WC()->session->save_data();
834 break;
835 }
836
837 if ($ok && $change) {
838 wp_send_json_success(array('msg'=>'order_change', 'url'=>''));
839 exit();
840 }
841 if ($ok) {
842 wp_send_json_success(array('msg'=>'no_change', 'url'=>''));
843 exit();
844 }
845
846 // This should never happen.
847 wp_send_json_success(array('msg'=>'unknown', 'url'=>''));
848 }
849
850 // Check cart total before initiating Vipps Checkout NT-2024-09-07
851 // Also any other checks we might want to do in the future. This will validate the cart each time the
852 // checkout page loads, even if a session is already in progress. IOK 2024-09-09
853 public function ajax_vipps_checkout_validate_cart() {
854 Vipps::set_locale_if_in_header();
855 $cart_total = WC()->cart->get_total('edit') ?: 0;
856 $minimum_amount = 1; // 1 in the store currency
857
858 if ($cart_total < $minimum_amount) {
859 wp_send_json_error(array(
860 'message' => sprintf(__('Vipps Checkout cannot be used for orders less than %1$s %2$s', 'woo-vipps'), $minimum_amount, get_woocommerce_currency() )
861 ));
862 } else {
863 wp_send_json_success(array('message', __("OK", 'woo-vipps')));
864 }
865 }
866
867 // Retrieve the current pending Vipps Checkout session, if it exists, and do some cleanup
868 // if it isn't correct IOK 2021-09-03
869 protected function vipps_checkout_current_pending_session () {
870 // If this is set, this is a currently pending order which is maybe still valid
871 $current_pending = is_a(WC()->session, 'WC_Session') ? WC()->session->get('vipps_checkout_current_pending') : false;
872 $order = $current_pending ? wc_get_order($current_pending) : null;
873
874 # If we do have an order, we need to check if it is 'pending', and if not, we have to check its payment status
875 $payment_status = null;
876 if ($order) {
877 if ($order->get_status() == 'pending') {
878 $payment_status = 'initiated'; // Just assume this for now
879 } else {
880 $payment_status = $order ? $this->gateway()->check_payment_status($order) : 'unknown';
881 }
882 }
883 // This covers situations where we can actually go directly to the thankyou-screen or whatever
884 $redirect = "";
885 if (in_array($payment_status, ['authorized', 'complete'])) {
886 $this->abandonVippsCheckoutOrder(false);
887 $redirect = $this->gateway()->get_return_url($order);
888 } elseif ($payment_status == 'cancelled') {
889 $this->log(sprintf(__("%1\$s session %2\$d cancelled (pending session)", 'woo-vipps'), Vipps::CheckoutName(), $order->get_id()), 'debug');
890 // This will mostly just wipe the session.
891 $this->abandonVippsCheckoutOrder($order);
892 // Previously we redirected to home_page() here, but in this case with a cancelled session,
893 // we want to keep the user at the Checkout and start a new session instead. LP 2026-03-17
894 }
895 // Now if we don't have an order right now, we should not have a session either, so fix that
896 if (!$order) {
897 $this->abandonVippsCheckoutOrder(false);
898 }
899
900 // Now check the orders vipps session if it exist
901 $session = $order ? $order->get_meta('_vipps_checkout_session') : false;
902
903 // A single word or array containing session data, containing token and frontendFrameUrl
904 // If a word, it will be ERROR EXPIRED FAILED IOK 2025-04-07
905 $session_status = $session ? $this->get_vipps_checkout_status($order) : null;
906
907 // If this is the case, there is no redirect, but the session is gone, so wipe the order and session.
908 if (in_array($session_status, ['ERROR', 'EXPIRED', 'FAILED'])) {
909 $this->log(sprintf(__("%1\$s session %2\$d is gone", 'woo-vipps'), Vipps::CheckoutName(), $order->get_id()), 'debug');
910 $this->abandonVippsCheckoutOrder($order);
911 }
912
913 // This will return either a valid vipps session, nothing, or redirect.
914 // From now on it could also return an order without a session. E.g. if the session was cancelled, we call abandonVippsCheckoutOrder and dont redirect anymore. LP 2026-03-17
915 return(array('order'=>$order ? $order->get_id() : false, 'session'=>$session, 'redirect'=>$redirect));
916 }
917
918 // Returns HTML of any widgets for the Checkout page IOK 2025-05-13
919 function vipps_ajax_get_widgets () {
920 $current_pending = is_a(WC()->session, 'WC_Session') ? WC()->session->get('vipps_checkout_current_pending') : false;
921 $order = $current_pending ? wc_get_order($current_pending) : null;
922 if (!$order) return "";
923
924 Vipps::set_locale_if_in_header();
925
926 print $this->get_checkout_widgets($order);
927 exit();
928 }
929
930 // This will, when visiting the cart or another checkout page and Vipps Mobilepay Checkout is active,
931 // remove any coupons that can't be both in the cart and in our current Checkout order (thus invalidating the order at the same time)
932 // but without the standard, now wrong error message produced in the cart for this. IOK 2025-05-15
933 public function prettily_cleanup_coupons_in_cart($silent=false) {
934 $cart = WC()->cart;
935 foreach ( $cart->get_applied_coupons() as $code ) {
936 $coupon = new WC_Coupon( $code );
937 if ( ! $coupon->is_valid() ) {
938 if (!$silent) {
939 $msg = sprintf(__("Your coupon code %s has been removed from your cart and your Checkout session has ended. You can add the code again either here or on the Checkout page", 'woo-vipps'), $code);
940 // Will only run in the legacy non-gutenberg cart
941 wc_add_notice($msg, 'notice');
942 }
943 $cart->remove_coupon( $code );
944 }
945 }
946 }
947
948 // This runs very early, in template-redirect; so we can add notices to the cart page. IOK 2025-05-15
949 // If coupons are added in Checkout after the order has been created, we need to change the error message
950 // in the Cart when the coupon is noticed as invalid there and removed. IOK 2025-05-15
951 public function handle_coupon_invalidation_in_checkout() {
952 global $post;
953 if ($post && is_page()) {
954 $gw = WC_Gateway_Vipps::instance();
955 $active = (wc_coupons_enabled() && $gw->get_option('vipps_checkout_enabled') == 'yes' && $gw->get_option('checkout_widget_coupon') === 'yes');
956 if ($active) {
957 if (has_block("woocommerce/cart")) {
958 $this->prettily_cleanup_coupons_in_cart();
959 } else {
960 // This is for the old shortcode-based cart; doing the remove several times is safe. IOK 2025-05-15
961 // Then add a new one that adds a different message, also reporting that the vipps session is gone
962 // Remove the standard validation code which reports an error
963 remove_action('woocommerce_check_cart_items', array(WC()->cart, 'check_cart_coupons'), 1);
964 add_action('woocommerce_check_cart_items', array($this, 'prettily_cleanup_coupons_in_cart'), 1);
965 }
966 }
967 }
968 }
969
970 // Define handlers for some default widgets (if active etc). IOK 2025-05-13
971 public function add_widget_callback_actions () {
972 add_filter('woo_vipps_checkout_callback_actions', function ($filters) {
973 $filters['submitnotes'] = function ($action, $order) {
974 $notes = isset($_REQUEST['callbackdata']['notes']) ? trim($_REQUEST['callbackdata']['notes']) : '';
975
976 // First delete latest customer order note if exists. LP 2025-05-14
977 $order_notes = $order->get_customer_order_notes();
978 $deleted = 0;
979 if ($order_notes) {
980 $latest_note = $order_notes[0];
981 if (is_a($latest_note, 'WP_Comment')) {
982 $deleted = wc_delete_order_note($latest_note->comment_ID);
983 }
984 }
985 $order->set_customer_note(sanitize_text_field($notes));
986 $order->save();
987
988 // Disable the email that gets sent on new order notes. IOK 2025-05-14
989 add_filter('woocommerce_mail_callback', function ($mailer, $mailclass) {
990 return '__return_true';
991 }, 999, 2);
992
993
994 // Add new note. LP 2025-05-14
995 if ($notes) {
996 $order->add_order_note($notes, 1, true);
997 return 1;
998 }
999 return 0;
1000 };
1001
1002 $filters['submitcoupon'] = function ($action, $order) {
1003 $code = isset($_REQUEST['callbackdata']['code']) ? trim($_REQUEST['callbackdata']['code']) : '';
1004
1005 if ($code) {
1006 add_filter('woocommerce_add_success', function ($message) { return ""; });
1007 add_filter('woocommerce_add_error', function ($message) { return ""; });
1008 add_filter('woocommerce_add_notice', function ($message) { return ""; });
1009
1010 do_action('woo_vipps_checkout_before_applying_coupon', $order, $code);
1011 if (WC()->cart) {
1012
1013
1014 $ok = WC()->cart->apply_coupon($code);
1015 if (!$ok || is_wp_error($ok)) {
1016 // IOK FIXME GET ACTUAL ERROR HERE
1017 throw (new Exception("Failed to apply coupon code $code"));
1018 }
1019
1020 $coupon = new WC_Coupon($code);
1021 $has_free = $coupon->get_free_shipping();
1022 add_filter('woo_vipps_checkout_recalculate_shipping', function ($should_recalc, $had_free, $order, $old_price) use ($has_free) {
1023 if ($has_free && !$had_free) {
1024 return true;
1025 }
1026 return $should_recalc;
1027 }, 10, 4);
1028
1029 }
1030
1031
1032 $res = $order->apply_coupon($code);
1033 if (is_wp_error($res)) throw (new Exception("Failed to apply coupon code $code"));
1034 do_action('woo_vipps_checkout_after_applying_coupon', $order, $code);
1035
1036 return 1;
1037 }
1038 return 0;
1039 };
1040
1041 $filters['removecoupon'] = function ($action, $order) {
1042 $code = isset($_REQUEST['callbackdata']['code']) ? trim($_REQUEST['callbackdata']['code']) : '';
1043 if ($code) {
1044 do_action('woo_vipps_checkout_before_removing_coupon', $order, $code);
1045 // Ensure the cart too loses the coupon
1046 if (WC()->cart) {
1047 $ok = WC()->cart->remove_coupon($code);
1048 // can't do much if this fails so
1049 }
1050 $res = $order->remove_coupon($code);
1051 do_action('woo_vipps_checkout_after_removing_coupon', $order, $code);
1052
1053 $coupon = new WC_Coupon($code);
1054 $has_free = $coupon->get_free_shipping();
1055 add_filter('woo_vipps_checkout_recalculate_shipping', function ($should_recalc, $had_free, $order, $old_price) use ($has_free) {
1056 if ($has_free && $had_free) {
1057 return true;
1058 }
1059 return $should_recalc;
1060 }, 10, 4);
1061
1062
1063 if ($res) return 1;
1064 }
1065 return 1; // just do it ? if errors happen here, the coupon *gets stuck*? FIXME IOK 2025-05-15
1066 };
1067 return $filters;
1068 });
1069 }
1070
1071
1072 // Add premade widgets depending on users settings. LP 2025-05-14
1073 // For now, coupon code widget and order notes widget.
1074 function maybe_add_widgets() {
1075 // Premade widget: coupon code. LP 2025-05-08
1076 $widgets = [];
1077 $use_widget_coupon = wc_coupons_enabled() && $this->gateway()->get_option('checkout_widget_coupon') === 'yes';
1078
1079 // Premade widget: order note. LP 2025-05-12
1080 $use_widget_ordernotes = $this->gateway()->get_option('checkout_widget_ordernotes') === 'yes';
1081 if ($use_widget_coupon || $use_widget_ordernotes) {
1082 add_filter('woo_vipps_checkout_widgets', function ($widgets) use ($use_widget_coupon, $use_widget_ordernotes) {
1083 if ($use_widget_coupon) {
1084 $widgets[] = [
1085 'title' => __('Coupon code', 'woo-vipps'),
1086 'id' => 'vipps_checkout_widget_coupon',
1087 'class' => 'vipps_checkout_widget_premade',
1088 'callback' => function($order) {?>
1089 <div id="vipps_checkout_widget_coupon_active_codes_container" style="display:none;">
1090 Active codes
1091 <div id="vipps_checkout_widget_coupon_active_codes_container_codes">
1092 <?php
1093 if ($order):
1094 foreach ($order->get_coupon_codes() as $code):?>
1095 <div class="vipps_checkout_widget_coupon_active_code_box" id="vipps_checkout_widget_coupon_active_code_<?php echo $code;?>">
1096 <span class="vipps_checkout_widget_coupon_active_code"><?php echo $code;?></span>
1097 <span class="vipps_checkout_widget_coupon_delete">✕</span>
1098 </div>
1099 <?php endforeach; endif;?>
1100 </div>
1101 </div>
1102 <form id="vipps_checkout_widget_coupon_form">
1103 <label for="vipps_checkout_widget_coupon_code" class="vipps_checkout_widget_small"><?php echo __('Enter your code', 'woo-vipps')?></label>
1104 <span id="vipps_checkout_widget_coupon_error" class="vipps_checkout_widget_error" style="display:none;"><?php echo __('Invalid coupon code', 'woo-vipps') ?></span>
1105 <span id="vipps_checkout_widget_coupon_delete_error" class="vipps_checkout_widget_error" style="display:none;"><?php echo __('Could not remove coupon', 'woo-vipps') ?></span>
1106 <span id="vipps_checkout_widget_coupon_success" class="vipps_checkout_widget_success" style="display:none;"><?php echo __('Coupon code added!', 'woo-vipps') ?></span>
1107 <input required id="vipps_checkout_widget_coupon_code" class="vipps_checkout_widget_input" type="text" name="code"/>
1108 <button type="submit" class="vippspurple2 vipps_checkout_widget_button"><?php echo __('Add', 'woo-vipps')?></button>
1109 </form>
1110 <?php
1111 }
1112 ];
1113 }
1114 if ($use_widget_ordernotes) {
1115 $widgets[] = [
1116 'title' => __('Order notes', 'woo-vipps'),
1117 'id' => 'vipps_checkout_widget_ordernotes',
1118 'class' => 'vipps_checkout_widget_premade',
1119 'callback' => function($order) { ?>
1120 <form id="vipps_checkout_widget_ordernotes_form">
1121 <div for="vipps_checkout_widget_ordernotes_input" class="vipps_checkout_widget_info"><?php echo __('Is there anything you wish to inform the store about? Include it here', 'woo-vipps')?></div>
1122 <label for="vipps_checkout_widget_ordernotes_input" class="vipps_checkout_widget_small"><?php echo __('Notes', 'woo-vipps')?></label>
1123 <span id="vipps_checkout_widget_ordernotes_error" class="vipps_checkout_widget_error" style="display:none;"><?php echo __('Something went wrong', 'woo-vipps') ?></span>
1124 <span id="vipps_checkout_widget_ordernotes_success" class="vipps_checkout_widget_success" style="display:none;"><?php echo __('Saved', 'woo-vipps') ?></span>
1125 <input id="vipps_checkout_widget_ordernotes_input" class="vipps_checkout_widget_input" type="text" name="notes" value="<?php if ($order) {
1126 $order_notes = $order->get_customer_order_notes();
1127 if ($order_notes) {
1128 $latest_note = $order_notes[0];
1129 if (is_a($latest_note, 'WP_Comment')) {
1130 echo $latest_note->comment_content;
1131 }
1132 }
1133 } ?>"/>
1134 <button type="submit" class="vippspurple2 vipps_checkout_widget_button"><?php echo __('Save', 'woo-vipps')?></button>
1135 </form>
1136 <?php
1137 }
1138 ];
1139 }
1140 return $widgets;
1141 });
1142 }
1143
1144 return $widgets;
1145 }
1146
1147 // This will display widgets like coupon codes, order notes etc on the Vipps Checkout page IOK 2025-05-02
1148 function get_checkout_widgets($order) {
1149 // Array of tables of [title, id, callback, class].
1150 // $default_widgets = $this->get_checkout_default_widgets($order);
1151 $this->maybe_add_widgets();
1152
1153 // NB: We may not have an order at this point. IOK 2025-05-02
1154 $widgets = apply_filters('woo_vipps_checkout_widgets', [], $order);
1155
1156 if (empty($widgets)) return "";
1157 ob_start();
1158 echo "<div class='vipps_checkout_widget_wrapper' style='display:none;'>";
1159 foreach ($widgets as $widget) {
1160 $id = $widget['id'] ?? "";
1161 $title = $widget['title'] ?? "";
1162 $class = $widget['class'] ?? "";
1163 $callback = $widget['callback'] ?? "";
1164
1165 if (!$title || !$callback) continue;
1166
1167 $idattr = $id ? "id='" . esc_attr($id) . "'" : "";
1168 $classattr = "class='vipps_checkout_widget" . ($class ? " " . esc_attr($class) : "") . "'";
1169 echo "<div $idattr $classattr>";
1170 echo "<div class='vipps_checkout_widget_title accordion'>" . esc_html($title) . "<span class='vipps_checkout_widget_icon'></span></div>";
1171 echo "<div class='vipps_checkout_body'>";
1172 call_user_func($callback, $order);
1173 echo "</div>";
1174 echo "</div>";
1175 }
1176 echo "</div>";
1177 $res = ob_get_clean();
1178
1179 return $res;
1180 }
1181
1182 function vipps_checkout_shortcode ($atts, $content) {
1183 // No point in expanding this unless we are actually doing the checkout. IOK 2021-09-03
1184 if (is_admin()) return;
1185 if (wp_doing_ajax()) return;
1186 if (defined('REST_REQUEST') && REST_REQUEST ) return;
1187 wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
1188 add_filter('woo_vipps_is_vipps_checkout', '__return_true');
1189
1190 // Defer to the normal code for endpoints IOK 2022-12-09
1191 if (is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' )) {
1192 return do_shortcode("[woocommerce_checkout]");
1193 }
1194
1195 if (!WC()->cart || WC()->cart->is_empty() ) {
1196 $this->abandonVippsCheckoutOrder(false);
1197 ob_start();
1198 wc_get_template( 'cart/cart-empty.php' );
1199 return ob_get_clean();
1200 }
1201
1202 WC()->session->set( 'chosen_payment_method', 'vipps'); // This is to stop KCO from trying to replace Vipps Checkout with KCO and failing. IOK 2024-05-13
1203
1204 // Previously registered, now enqueue this script which should then appear in the footer.
1205 // Then call a hook for people adding custom javascript. This needs to be moved to template redirect. IOK 2025-06-02
1206 wp_enqueue_script('vipps-checkout');
1207 do_action('woo_vipps_checkout_enqueue_scripts');
1208
1209 do_action('vipps_checkout_before_get_session');
1210
1211 // We need to be able to check if we still have a live, good session, in which case
1212 // we can open the iframe directly. Otherwise, the form we are going to output will
1213 // create the iframe after a button press which will create a new order.
1214 $sessioninfo = $this->vipps_checkout_current_pending_session();
1215
1216 $out = ""; // Start generating output already to make debugging easier
1217
1218 // This is the current pending order id, if it exists. Will be used to restart orders etc . IOK 2023-08-15 FIXME
1219 $current_pending = is_a(WC()->session, 'WC_Session') ? WC()->session->get('vipps_checkout_current_pending') : false;
1220
1221 if ($sessioninfo['redirect']) {
1222 // This is always either the thankyou page or home_url() IOK 2021-09-03
1223 $redir = json_encode($sessioninfo['redirect']);
1224 $out .= "<script>window.location.replace($redir);</script>";
1225 return $out;
1226 }
1227
1228 // Now the normal case.
1229 $errortext = apply_filters('woo_vipps_checkout_error', __('An error has occured - please reload the page to restart your transaction, or return to the shop', 'woo-vipps'));
1230 $expiretext = apply_filters('woo_vipps_checkout_error', __('Your session has expired - please reload the page to restart, or return to the shop', 'woo-vipps'));
1231
1232 $out .= Vipps::instance()->spinner();
1233
1234 if (!$sessioninfo['session']) {
1235 $out .= "<div style='visibility:hidden' class='vipps_checkout_startdiv'>";
1236 $out .= "<h2>" . sprintf(__('Press the button to complete your order with %1$s!', 'woo-vipps'), Vipps::instance()->get_payment_method_name()) . "</h2>";
1237 $out .= '<div class="vipps_checkout_button_wrapper" ><button type="submit" class="button vipps_checkout_button vippsorange" value="1">' . sprintf(__('%1$s', 'woo-vipps'), Vipps::CheckoutName()) . '</button></div>';
1238 $out .= "</div>";
1239 }
1240
1241 // If we have an actual live session right now, add it to the page on load. Otherwise, the session will be started using ajax after the page loads (and is visible)
1242 if ($sessioninfo['session']) {
1243 $token = $sessioninfo['session']['token']; // From Vipps
1244 $src = $sessioninfo['session']['checkoutFrontendUrl']; // From Vipps
1245 $out .= "<script>VippsSessionState = " . json_encode(array('token'=>$token, 'checkoutFrontendUrl'=>$src)) . ";</script>\n";
1246 } else {
1247 $out .= "<script>VippsSessionState = null;</script>\n";
1248 }
1249
1250 // Mount point for widgets. IOK 2025-05-13
1251 // starts hidden. is shown when vipps checkout loads successfully. LP 2025-05-12
1252 $out .= "<div id='vippscheckoutframe'>";
1253 $out .= "<div id='vipps_checkout_widget_mount'></div>";
1254
1255 $out .= "</div>";
1256 $out .= "<div style='display:none' id='vippscheckouterror'><p>$errortext</p></div>";
1257 $out .= "<div style='display:none' id='vippscheckoutexpired'><p>$expiretext</p></div>";
1258
1259
1260 // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
1261 $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
1262 $out .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
1263 $out .= "<input type='hidden' id='vippsorderid' name='_vippsorder' value='" . intval($current_pending) . "' />";
1264 // And this is for the order attribution feature of Woo 8.5 IOK 2024-01-09
1265 if (WC_Gateway_Vipps::instance()->get_option('vippsorderattribution') == 'yes') {
1266 $out .= '<input type="hidden" id="vippsorderattribution" value="1" />';
1267 ob_start();
1268 do_action( 'woocommerce_after_order_notes');
1269 $out .= ob_get_clean();
1270 }
1271 $out .= wp_nonce_field('do_vipps_checkout','vipps_checkout_sec',1,false);
1272 $out .= "</form>";
1273
1274 return $out;
1275 }
1276
1277
1278 public function cart_changed() {
1279 // Don't do this if we are changing the cart in a Vipps Checkout callback. IOK 2025-05-15
1280 if (apply_filters('woo_vipps_is_checkout_callback', false)) {
1281 return;
1282 }
1283 $current_pending = is_a(WC()->session, 'WC_Session') ? WC()->session->get('vipps_checkout_current_pending') : false;
1284 $order = $current_pending ? wc_get_order($current_pending) : null;
1285 if (!$order) return;
1286 $this->log(sprintf(__("%1\$s: cart changed while session %2\$d in progress - now cancelled", 'woo-vipps'), Vipps::CheckoutName(), $order->get_id()), 'debug');
1287 $this->abandonVippsCheckoutOrder($order);
1288 }
1289
1290 public function abandonVippsCheckoutOrder($order) {
1291
1292 if (WC()->session) {
1293 WC()->session->set('vipps_checkout_current_pending',0);
1294 WC()->session->set('vipps_address_hash', false);
1295 }
1296
1297 if (is_a($order, 'WC_Order') && $order->get_status() == 'pending') {
1298 // We want to kill orders that have failed, or that the user has abandoned. To do this,
1299 // we must ensure that no race or other mechanism kills the order while or after being paid.
1300 // if order is in the process of being finalized, don't kill it
1301 if (Vipps::instance()->isLocked($order)) {
1302 return false;
1303 }
1304 // Get it again to ensure we have all the info, and check status again
1305 clean_post_cache($order->get_id());
1306 $order = wc_get_order($order->get_id());
1307 if ($order->get_status() != 'pending') return false;
1308
1309 // And to be extra sure, check status at vipps
1310 $session = $order->get_meta('_vipps_checkout_session');
1311 if (!$session) return false;
1312
1313 try {
1314 $polldata = $this->gateway()->api->checkout_get_session_info($order);
1315 $sessionState = (!empty($polldata) && is_array($polldata) && isset($polldata['sessionState'])) ? $polldata['sessionState'] : "";
1316 $this->log("Checking Checkout status on cart/order change for " . $order->get_id() . " $sessionState ", 'debug');
1317 if ($sessionState == 'PaymentSuccessful' || $sessionState == 'PaymentInitiated') {
1318 // If we have started payment, we do not kill the order.
1319 $this->log("Checkout payment started - cannot cancel for " . $order->get_id(), 'debug');
1320 return false;
1321 }
1322 } catch (Exception $e) {
1323 $this->log(sprintf(__('Could not get Checkout status for order %1$s in progress while cancelling', 'woo-vipps'), $order->get_id()), 'debug');
1324 }
1325
1326
1327 // NB: This can *potentially* be revived by a callback!
1328 $this->log(sprintf(__('Cancelling Checkout order because order changed: %1$s', 'woo-vipps'), $order->get_id()), 'debug');
1329 $order->set_status('cancelled', __("Order specification changed - this order abandoned by customer in Checkout ", 'woo-vipps'), false);
1330 // Also mark for deletion.
1331 $order->update_meta_data('_vipps_delendum',1);
1332 $order->save();
1333 }
1334 }
1335
1336 public function get_vipps_checkout_status($order) {
1337 $status = $this->gateway()->api->checkout_get_session_info($order);
1338 return $status;
1339 }
1340
1341
1342 public function maybe_override_checkout_page_id ($id) {
1343 // Only do this if Vipps Checkout was ever activated
1344 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
1345 if (!$vipps_checkout_activated) return $id;
1346
1347 // The gutenberg block (and other pages) calls the checkout-page-id function *a lot* so let's just check once
1348 if ($this->payid) return $this->payid;
1349
1350 // If we are on a checkout page, don't go other places please
1351 if (is_page()){
1352 global $post;
1353 // The unfiltered checkout page from woo
1354 if ($post && $post->ID == get_option( 'woocommerce_checkout_page_id' )) {
1355 $this->payid = $id;
1356 return $id;
1357 }
1358 // any other page with a gutenberg checkout block. We don't need to test for the shortcode, that works fine.
1359 if ($post && has_block( 'woocommerce/checkout', $post->post_content) ) {
1360 $this->payid = $id;
1361 return $id;
1362 }
1363 // If this is "pay for order", also don't do anything.
1364 $orderid = absint(get_query_var( 'order-pay'));
1365 if ($orderid) {
1366 $this->payid = $id;
1367 return $id;
1368 }
1369 }
1370
1371 // Else, if Vipps Checkout is enabled, can be used etc, use that.
1372 $checkoutid = $this->gateway()->vipps_checkout_available();
1373 if ($checkoutid) {
1374 $this->payid = $checkoutid;
1375 return $checkoutid;
1376 }
1377
1378 return $id;
1379 }
1380
1381 public function woocommerce_loaded () {
1382 # This implements the Vipps Checkout replacement checkout page for those that wants to use that, by filtering the checkout page id.
1383 add_filter('woocommerce_get_checkout_page_id', array($this, 'maybe_override_checkout_page_id'), 10, 1);
1384
1385 // This is for the 'other payment method' thing in Vipps Checkout - we store address info
1386 // in session. IOK 2024-05-13
1387 add_filter('woocommerce_checkout_fields', function ($fields) {
1388 if (empty(WC()->session)) return $fields;
1389 $possibly_address = WC()->session->get('vc_address');
1390
1391 if (!$possibly_address) return $fields;
1392 WC()->session->set('vc_address', null);
1393
1394 foreach($fields['billing'] as $key => &$bdata) {
1395 $v = trim($possibly_address[$key] ?? "");
1396 if ($v) {
1397 $bdata['default'] = $v;
1398 }
1399 }
1400 foreach($fields['shipping'] as $key => &$sdata) {
1401 $v = trim($possibly_address[$key] ?? "");
1402 if ($v) {
1403 $sdata['default'] = $v;
1404 }
1405 }
1406 return $fields;
1407 });
1408
1409 }
1410
1411
1412 public function woocommerce_settings_pages ($settings) {
1413 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
1414 if (!$vipps_checkout_activated) return $settings;
1415 $i = -1;
1416 foreach($settings as $entry) {
1417 $i++;
1418 if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
1419 break;
1420 }
1421 }
1422 if ($i > 0) {
1423
1424 $vippspagesettings = array(
1425 array(
1426 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CheckoutName()),
1427 'desc' => sprintf(__('This page is used for the alternative %1$s page, which you can choose to use instead of the normal WooCommerce checkout page. ', 'woo-vipps'), Vipps::CheckoutName()) . sprintf( __( 'Page contents: [%1$s]', 'woocommerce' ), 'vipps_checkout') ,
1428 'id' => 'woocommerce_vipps_checkout_page_id',
1429 'type' => 'single_select_page_with_search',
1430 'default' => '',
1431 'class' => 'wc-page-search',
1432 'css' => 'min-width:300px;',
1433 'args' => array(
1434 'exclude' =>
1435 array(
1436 wc_get_page_id( 'myaccount' ),
1437 ),
1438 ),
1439 'desc_tip' => true,
1440 'autoload' => false,
1441 ));
1442 array_splice($settings, $i, 0, $vippspagesettings);
1443 }
1444
1445 return $settings;
1446 }
1447
1448
1449 // Translate from the Express Checkout shipping method format to the Vipps Checkout shipping
1450 // format, which is slightly different. The ratemap maps from a method key to its WC_Shipping_Rate, and the method map does
1451 // the same for WP_Shipping_Method.
1452 // The ratemap will in the end be stored in the order and used to retrieve the selected shipping method. IOK 2025-08-15
1453 // IOK 2025-05-07 Also treat PickupLocation specially. We'll return at most one of these, and if there are more than one, we will add the locations available as
1454 // metadata.
1455 public function format_shipping_methods ($return, &$ratemap, $methodmap, $order) {
1456 $translated = array();
1457 $currency = get_woocommerce_currency();
1458 $pickupLocation = null; // if we have a pickup_location rate, set this to be the first one. IOK 2025-05-07
1459
1460 foreach ($return['shippingDetails'] as $m) {
1461 $m2 = array();
1462
1463 $m2['isDefault'] = (bool) (($m['isDefault']=='Y') ? true : false); // type bool here, but not in the other api
1464 $m2['priority'] = $m['priority'];
1465 $m2['amount'] = array(
1466 'value' => round(100*$m['shippingCost']), // Unlike eComm, this uses cents
1467 'currency' => $currency // May want to use the orders' currency instead here, since it exists.
1468 );
1469 $m2['brand'] = "OTHER";
1470 $m2['title'] = $m['shippingMethod'];
1471 $m2['id'] = $m['shippingMethodId'];
1472
1473 $rate = $ratemap[$m2['id']];
1474 $shipping_method = $methodmap[$m2['id']];
1475
1476 // If we have pickup_location-s, only use the first one. IOK 2025-05-07
1477 if ($rate->method_id == 'pickup_location') {
1478 if (!$pickupLocation) {
1479 $pickupLocation = &$m2;
1480 } else {
1481 continue;
1482 }
1483 }
1484
1485
1486 // Some data must be visible in the Order screen, so add meta data, also, for dynamic pricing check that free shipping hasn't been reached
1487 $meta = $rate->get_meta_data();
1488
1489 // The description is normally only stored only in the shipping method
1490 if ($shipping_method) {
1491 // Support dynamic cost alongside free shipping using the new api where NULL is dynamic pricing 2023-07-17
1492 if (isset($shipping_method->instance_settings['dynamic_cost']) && $shipping_method->instance_settings['dynamic_cost'] == 'yes') {
1493 if (!isset($meta['free_shipping']) || !$meta['free_shipping']) {
1494 $m2['amount'] = null;
1495 }
1496 }
1497 $m2['description'] = $shipping_method->get_option('description', '');
1498 } else {
1499 $m2['description'] = "";
1500 }
1501
1502
1503 // Allow shipping methods to add pickup points data IOK 2025-04-08
1504 $delivery = [];
1505 $pickup_points = apply_filters('woo_vipps_shipping_method_pickup_points', [], $rate, $shipping_method, $order);
1506 if ($pickup_points) {
1507 $filtered = [];
1508 foreach($pickup_points as $point) {
1509 $ok = true;
1510 $entry = [];
1511 foreach(['address', 'city', 'country', 'id', 'name', 'postalCode'] as $key) {
1512 if (!isset($point[$key])) {
1513 $this->log(__('Cannot add pickup point: A pickup point needs to have keys id, name, address, city, postalCode and country: ', 'woo-vipps') . print_r($point, true), 'error');
1514 $ok = false;
1515 break;
1516 } else {
1517 $entry[$key] = $point[$key];
1518 }
1519 }
1520 foreach(['openingHours', 'leadTime'] as $key) {
1521 if (isset($point[$key])) {
1522 $entry[$key] = $point[$key];
1523 }
1524 }
1525
1526 if ($ok && !empty($entry)) {
1527 $filtered[] = $entry;
1528 }
1529 }
1530 $delivery['pickupPoints'] = $filtered;
1531 $m2['type'] = 'PICKUP_POINT';
1532
1533 // Remove name of location for PickupLocation if we do have choices. IOK 2025-05-07
1534 if ($m2 == $pickupLocation && count($filtered) > 1) {
1535 $m2['title'] = $shipping_method->title;
1536 }
1537
1538
1539 }
1540
1541 // Timeslots. This is for home delivery options, should have values id (string), date (date), start (time), end (time).
1542 // IOK 2025-04-10
1543 $timeslots = apply_filters('woo_vipps_shipping_method_timeslots', [], $rate, $shipping_method, $order);
1544 if (!empty($timeslots)) {
1545 $filtered = [];
1546 foreach($timeslots as $timeslot) {
1547 $entry = [];
1548 $ok = true;
1549 foreach(['id', 'date', 'start', 'end'] as $key) {
1550 if (!isset($timeslot[$key])) {
1551 $this->log(__('Cannot add timeslot: A timeslot needs to have keys id, date, start and end: ', 'woo-vipps') . print_r($timeslot, true), 'error');
1552 $ok = false;
1553 break;
1554 } else {
1555 $entry[$key] = $timeslot[$key];
1556 }
1557 }
1558 if ($ok && !empty($entry)) {
1559 $filtered[] = $entry;
1560 }
1561 }
1562 $delivery['timeslots']=$filtered;
1563 $m2['type'] = 'HOME_DELIVERY';
1564 }
1565
1566 // add leadTime data to "Mailbox" types
1567 $leadTime = apply_filters('woo_vipps_shipping_method_lead_time', null, $rate, $shipping_method, $order);
1568 if (!empty($leadTime)) {
1569 $entry = [];
1570 $ok = true;
1571 foreach(['earliest', 'latest'] as $key) {
1572 if (!isset($leadTime[$key])) {
1573 $ok = false; break;
1574 }
1575 $entry[$key] = $leadTime[$key];
1576 }
1577 if ($ok && !empty($entry)) {
1578 $delivery['leadTime'] = $entry;
1579 }
1580 }
1581
1582 if (!empty($delivery)) {
1583 $m2['delivery'] = $delivery;
1584 }
1585
1586 if (isset($meta['brand'])) {
1587 $m2['brand'] = $meta['brand'];
1588 } else {
1589 // specialcase some known methods so they get brands, and put the label into the description
1590 if ($shipping_method && is_a($shipping_method, 'WC_Shipping_Method') && get_class($shipping_method) == 'WC_Shipping_Method_Bring_Pro') {
1591 $m2['brand'] = "POSTEN";
1592 $m2['description'] = $rate->get_label();
1593 }
1594 $m2['brand'] = apply_filters('woo_vipps_shipping_method_brand', $m2['brand'],$shipping_method, $rate);
1595 }
1596
1597 if ($m2['brand'] != "OTHER" && isset($meta['type'])) {
1598 $m2['type'] = $meta['type'];
1599 if ($m2['brand'] === 'POSTI') {
1600 $m2['type'] = 'PICKUP_POINT'; // Temp fix. Posti only supports pickup point now. LP 2025-10-17
1601 }
1602 }
1603
1604 // Old filter kept for backwards compatibility
1605 $m2['description'] = apply_filters('woo_vipps_shipping_method_description', $m2['description'], $rate, $shipping_method);
1606 $translated[] = $m2;
1607 }
1608
1609 $return['shippingDetails'] = $translated;
1610 unset($return['addressId']); // Not used it seems for checkout
1611 unset($return['orderId']);
1612
1613 $return = apply_filters('woo_vipps_checkout_json_shipping_methods', $return, $order);
1614 return $return;
1615 }
1616
1617
1618
1619 }
1620