PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.5
Pay with Vipps and MobilePay for WooCommerce v6.2.5
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 / payment / VippsCheckout.class.php

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

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