PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 5.3.4
Pay with Vipps and MobilePay for WooCommerce v5.3.4
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 5.4.1 5.4.0 5.3.4 All 184 releases
woo-vipps / payment / VippsCheckout.class.php

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

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