PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.3.1
Pay with Vipps and MobilePay for WooCommerce v6.3.1
6.3.1 6.2.6 6.3.0 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 All 190 releases
← All changes | payment/Vipps.class.php +907 -842 6.1.1 → 6.3.1 View file →
@@ -54,11 +54,8 @@
54 54
55 55 // True if HPOS is being used
56 56 public $HPOSActive = null;
57 57
58 - // used in the fake locking mechanism using transients
59 - private $lockKey = null;
60 -
61 58 public $vippsJSConfig = array();
62 59
63 60 public $button_options_version = '2.0';
64 61 public $button_options_express_version = '2.0';
@@ -70,9 +67,9 @@
70 67 public static function CheckoutName($order=null) {
71 68 return "Vipps MobilePay Checkout"; // Do not translate
72 69 }
73 70 public static function ExpressCheckoutName($order=null) {
74 - return __("Vipps Express Checkout", 'woo-vipps');
71 + return __("Vipps MobilePay Express Checkout", 'woo-vipps');
75 72 }
76 73 public static function LoginName() {
77 74 return __("Login with Vipps", 'woo-vipps');
78 75 }
@@ -117,16 +114,22 @@
117 114 add_action('wp_footer', array($Vipps,'footer'));
118 115 }
119 116 add_action( 'plugins_loaded', array($Vipps,'plugins_loaded'));
120 117 add_action( 'after_setup_theme', array($Vipps,'after_setup_theme'));
121 - add_action('init',array($Vipps,'init'));
118 + add_action( 'init',array($Vipps,'init'));
119 + add_action( 'rest_api_init', array($Vipps, 'rest_api_init'));
122 120 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
123 121 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
124 122 add_action( 'woocommerce_blocks_loaded', [$Vipps, 'woocommerce_blocks_loaded']);
125 - // Express Checkout and Vipps Checkout supports the new pickup_location shipping method, but the admin interface for this may
123 + // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
126 124 // not have loaded if the default checkout solution isn't the Checkout block. We'll load it anyway if the user has any local pickup locations
127 125 // stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25
128 126 add_action('woocommerce_load_shipping_methods', array($Vipps, 'maybe_load_pickup_locations'), 90);
127 +
128 + // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
129 + // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
130 + // is important.
131 + add_filter('woocommerce_create_pages', array($Vipps, 'woocommerce_create_pages'), 50, 1);
129 132 }
130 133
131 134 // Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10
132 135 public function woocommerce_blocks_loaded() {
@@ -222,8 +225,9 @@
222 225 // Register certain scripts in wp_loaded because they will be added to the backend as well - the gutenberg checkout block
223 226 // needs these to be defined in the backend. IOK 2024-04-16
224 227 add_action('wp_loaded', array($this, 'wp_register_scripts'));
225 228 add_action('wp_enqueue_scripts', array($this, 'wp_enqueue_scripts'));
229 + add_action('wp_enqueue_scripts', array($this, 'enqueue_classic_checkout_scripts'), 20);
226 230
227 231 // Remove the possibility of restarting failed orders etc. This will be fixed in the future. IOK 2023-05-26
228 232 add_filter('woocommerce_my_account_my_orders_actions', array($this,'woocommerce_my_account_my_orders_actions'), 10, 2);
229 233
@@ -239,16 +243,8 @@
239 243
240 244 // Extra order actions on the order screen, now using ajax to be compatible with HPOS. IOK 2022-12-02
241 245 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
242 246
243 - // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
244 - add_action('rest_api_init', function() {
245 - register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
246 - 'methods' => 'GET',
247 - 'callback' => [$this, 'rest_express_checkout_products'],
248 - 'permission_callback' => '__return_true',
249 - ]);
250 - });
251 247
252 248 // We need a 5-minute scheduled event for the handler for missed callbacks. Using the
253 249 // action scheduler would be better, but we can't do that just yet because of backwards
254 250 // compatibility. At some point, support for older woo-versions should be dropped; then this
@@ -271,19 +267,65 @@
271 267 // because it is self-updating or because it has been deactivated just now or something, we won't have access to it.
272 268 // Therefore test it first. IOK 2022-12-08
273 269 $gw = $this->gateway();
274 270
275 - // This is a developer-mode level feature because flock() is not portable. This ensures callbacks and shopreturns do not
276 - // simultaneously update the orders, in particular not the express checkout order lines wrt shipping. IOK 2020-05-19
277 - if ($gw && $gw->get_option('use_flock') == 'yes') {
278 - add_filter('woo_vipps_lock_order', array($this,'flock_lock_order'));
279 - add_action('woo_vipps_unlock_order', array($this, 'flock_unlock_order'));
280 - }
281 -
282 271 // Set default button options, migrating any older setup IOK 2026-07-15
283 272 $this->init_button_options();
273 +
274 + /*
275 + From version 6.2.x we create a real physical page to handle the "special" vipps pages,
276 + where we earlier used just a fake page with no real page id, unless especially configured.
277 + We therefore need to add code to maintain this special page.
278 +
279 + woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
280 + and we hook unto this with woocommerce_create_pages. LP 2026-09-03
281 + */
282 +
283 + // Delete special page id option when its deleted or trashed, so that we dont have to load
284 + // in the post to check status in woocommerce_loaded when we ensure the special page exists. LP 2026-09-03
285 + $delete_special_page_id = function($post_id, $post = null) {
286 + if (static::get_special_page_id() === $post_id) {
287 + delete_option('woocommerce_vipps_special_page_page_id');
288 + }
289 + };
290 + add_action('delete_post', $delete_special_page_id, 10, 2);
291 + add_action('wp_trash_post', $delete_special_page_id, 10, 2);
292 +
293 + $this->ensure_special_page_exists();
294 +
295 +
296 + // We want this special page to have a certain title and maybe special scripts and so on,
297 + // this gets run in template redirect for these pages.
298 + add_action('woo_vipps_before_handling_special_page', array($this, 'pre_special_page_actions'));
299 +
300 + // Add an admin interface for this page as well IOK 2026-09-11
301 + add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
284 302 }
285 303
304 +
305 + public function rest_api_init () {
306 +
307 + // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
308 + register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
309 + 'methods' => 'GET',
310 + 'callback' => [$this, 'rest_express_checkout_products'],
311 + 'permission_callback' => '__return_true',
312 + ]);
313 +
314 + // Start a single product express checkout process. IOK 2026-08-25
315 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout_single', [
316 + 'methods' => 'POST',
317 + 'callback' => [$this, 'rest_do_single_product_express_checkout'],
318 + 'permission_callback' => '__return_true',
319 + ]);
320 + // And one for the cart. IOK 2026-09-04
321 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout', [
322 + 'methods' => 'POST',
323 + 'callback' => [$this, 'rest_do_express_checkout'],
324 + 'permission_callback' => '__return_true',
325 + ]);
326 + }
327 +
286 328 public function admin_init () {
287 329 $gw = $this->gateway();
288 330 require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php");
289 331 $adminSettings = VippsAdminSettings::instance();
@@ -310,10 +352,8 @@
310 352 // Styling etc
311 353 add_action('admin_head', array($this, 'admin_head'));
312 354
313 355 // Scripts
314 - $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
315 - wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
316 356 add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts'));
317 357
318 358 // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
319 359 add_action('current_screen', function ($screen) {
@@ -401,9 +441,77 @@
401 441 }
402 442 }
403 443 }
404 444
445 +
446 + /** Ensure we have a special page for payment flows
447 + *
448 + * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
449 + * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
450 + **/
451 + public function ensure_special_page_exists() {
452 + if (static::get_special_page_id()) return;
453 + $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
405 454
455 + // If user had in previous version overriden the fake page with a real one: migrate this page to be the special page. LP 2026-09-01
456 + $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
457 + if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
458 + $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
459 + // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
460 + update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
461 +
462 + // Ensure this page has the necessary shortcode. LP 2026-09-01
463 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
464 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
465 + wp_update_post([
466 + 'ID' => $old_special_page_id,
467 + 'post_content' => $new_content,
468 + ]);
469 + }
470 + } else {
471 + // Create special page if its missing. LP 2026-09-01
472 + $this->maybe_create_vipps_pages();
473 + }
474 + }
475 +
476 + // Admin interface for the special page on woo/advanced/pages
477 + public function woocommerce_settings_pages ($settings) {
478 + $i = -1;
479 + foreach($settings as $entry) {
480 + $i++;
481 + if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
482 + break;
483 + }
484 + }
485 + if ($i > 0) {
486 + $vippspagesettings = array(
487 + array(
488 + 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
489 + 'desc' => sprintf(__('This page is used for various special pages used by %1$s', 'woo-vipps'), Vipps::CompanyName()) . sprintf( __( 'Page contents: [%1$s]', 'woocommerce' ), 'vipps_special_page') ,
490 + 'id' => 'woocommerce_vipps_special_page_page_id',
491 + 'type' => 'single_select_page_with_search',
492 + 'default' => '',
493 + 'class' => 'wc-page-search',
494 + 'css' => 'min-width:300px;',
495 + 'args' => array(
496 + 'exclude' =>
497 + array(
498 + wc_get_page_id( 'myaccount' ),
499 + wc_get_page_id( 'checkout' ),
500 + wc_get_page_id( 'cart' ),
501 + ),
502 + ),
503 + 'desc_tip' => true,
504 + 'autoload' => false,
505 + ));
506 + array_splice($settings, $i, 0, $vippspagesettings);
507 + }
508 +
509 + return $settings;
510 + }
511 +
512 +
513 +
406 514 // Runs on init, adds the Vipps badge feature if activated
407 515 public function maybe_add_vipps_badge_feature () {
408 516 $badge_options = get_option('vipps_badge_options');
409 517 if (!$badge_options || !@$badge_options['badgeon']) return false;
@@ -732,8 +840,11 @@
732 840
733 841 // Get current brand and language
734 842 $current_brand = strtolower($this->get_payment_method_name());
735 843 $current_language = $this->get_customer_language();
844 + if ('se' === $current_language) $current_language = 'sv';
845 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
846 + if ('dk' === $current_language) $current_language = 'da';
736 847
737 848 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
738 849 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
739 850 'purple'=> __('Purple', 'woo-vipps')];
@@ -796,9 +907,9 @@
796 907
797 908 <h2><?php _e('Shortcodes', 'woo-vipps'); ?> </h2>
798 909 <p><?php echo sprintf(__('If you need to add a %1$s badge on a specific page, footer, header and so on, and you cannot use the Gutenberg Block provided for this, you can either add the %1$s Badge manually (as <a href="%2$s" nofollow rel=nofollow target=_blank>documented here</a>) or you can use the shortcode.', 'woo-vipps'), Vipps::CompanyName(), "https://developer.vippsmobilepay.com/docs/knowledge-base/design-guidelines/on-site-messaging/"); ?></p>
799 910 <br><?php _e("The shortcode looks like this:", 'woo-vipps')?><br>
800 - <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|dk} ] </pre><br>
911 + <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|da|sv} ] </pre><br>
801 912 <?php _e("Please refer to the documentation for the meaning of the parameters.", 'woo-vipps'); ?></br>
802 913 <?php _e("The brand will be automatically applied.", 'woo-vipps'); ?>
803 914 </p>
804 915
@@ -876,9 +987,12 @@
876 987 public function vipps_mobilepay_badge_shortcode($atts) {
877 988 $args = shortcode_atts( array('id'=>'', 'class'=>'', 'brand' => '', 'variant' => '','language'=>''), $atts );
878 989
879 990 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple', 'filled', 'light']) ? $args['variant'] : "";
880 - $language = in_array($args['language'], ['en','no', 'fi', 'dk']) ? $args['language'] : $this->get_customer_language();
991 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
992 + if ('se' === $language) $language = 'sv';
993 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
994 + if ('dk' === $language) $language = 'da';
881 995 $id = sanitize_title($args['id']);
882 996 $class = sanitize_text_field($args['class']);
883 997
884 998 $attributes = [];
@@ -894,13 +1008,18 @@
894 1008 return "<vipps-mobilepay-badge $badgeatts></vipps-mobilepay-badge>";
895 1009 }
896 1010
897 1011 // legacy vipps_badge shortcode, the new one is vipps_mobilepay_badge_shortcode. LP 19.11.2024
1012 + // Diff: this one doesn't support brand (JUST VIPPS). LP 2026-08-11
898 1013 public function vipps_badge_shortcode($atts) {
899 1014 $args = shortcode_atts( array('id'=>'', 'class'=>'','variant' => '','language'=>''), $atts );
900 1015
901 1016 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple']) ? $args['variant'] : "";
902 - $language = in_array($args['language'], ['en','no', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1017 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1018 + if ('se' === $language) $language = 'sv';
1019 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1020 + if ('dk' === $language) $language = 'da';
1021 +
903 1022 $id = sanitize_title($args['id']);
904 1023 $class = sanitize_text_field($args['class']);
905 1024
906 1025 $attributes = [];
@@ -929,12 +1048,30 @@
929 1048
930 1049 public function get_html_button_attrs_for_context($context = 'global') {
931 1050 $options = get_option('vipps_button_options2', []);
932 1051 if (!is_string($context)) $context = 'global';
1052 +
1053 + // Gutenberg express checkout buttons really want to be stretched, so we'll treat them somewhat differently.
1054 + $gutenberg = false;
1055 + if ($context == 'checkout_gutenberg') {
1056 + $context = 'checkout';
1057 + $gutenberg = true;
1058 + }
1059 + if ($context == 'cart_gutenberg') {
1060 + $context = 'cart';
1061 + $gutenberg = true;
1062 + }
1063 +
933 1064 $config = $options['express']['configs'][$context] ?? [];
934 - if (!$config || ($config['use-global-config'] ?? false)) {
1065 + $use_global = !$config || ($config['use-global-config'] ?? false);
1066 + if ($use_global) {
935 1067 $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
936 1068 }
1069 +
1070 + // see above.
1071 + if ($gutenberg) {
1072 + $config['stretched']='true';
1073 + }
937 1074 return $config;
938 1075 }
939 1076
940 1077 public function get_html_button_for_context($context = 'global') {
@@ -952,8 +1089,12 @@
952 1089 // Support using store language
953 1090 if ('store' === $attrs['language']) $attrs['language'] = $this->get_customer_language();
954 1091 // Don't support these login verbs. LP 2026-06-04
955 1092 if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy';
1093 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1094 + if ('dk' === $attrs['language']) $attrs['language'] = 'da';
1095 + // Fix swedish too. LP 2026-10-06
1096 + if ('se' === $attrs['language']) $attrs['language'] = 'sv';
956 1097
957 1098 $escaped_attrs = [];
958 1099 foreach($attrs as $k => $v) {
959 1100 $escaped_attrs[$k] = esc_attr($v);
@@ -1019,8 +1160,10 @@
1019 1160 private function button_menu_express_section() {
1020 1161 $options = get_option('vipps_button_options2', []);
1021 1162 $express = $options['express'] ?? [];
1022 1163 $configs = $express['configs'] ?? [];
1164 +
1165 +
1023 1166 $contexts = [
1024 1167 'global' => __('Global', 'woo-vipps'),
1025 1168 'product' => __('Product', 'woo-vipps'),
1026 1169 'catalog' => __('Catalog', 'woo-vipps'),
@@ -1151,9 +1294,9 @@
1151 1294
1152 1295 // Update the preview web component's attributes. LP 2026-06-24
1153 1296 function updatePreview(event) {
1154 1297 const args = getPreviewArgs();
1155 - // LP FIXME: when i use get_customer_language() here it gives me my user language, but on frontend it gives the site language, i.e not the same value. So this preview will be wrong language. so use get_locale for now. LP 2026-07-02
1298 + // FIXME: when i use get_customer_language() here it gives me my user language, but on frontend it gives the site language, i.e not the same value. So this preview will be wrong language. so use get_locale for now. LP 2026-07-02
1156 1299 // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1157 1300 if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1158 1301 const button = jQuery('#vipps-button-express-preview');
1159 1302 button.attr(args);
@@ -1193,8 +1336,9 @@
1193 1336
1194 1337 // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1195 1338 const newContext = jQuery("#context").val();
1196 1339 const newConfig = contextConfigs[newContext];
1340 +
1197 1341 setInputsFromConfig(newContext, newConfig);
1198 1342 currentContext = newContext;
1199 1343 }
1200 1344
@@ -1557,12 +1701,14 @@
1557 1701 <?php
1558 1702 }
1559 1703 // Scripts used in the backend
1560 1704 public function admin_enqueue_scripts($hook) {
1705 +
1706 + wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1707 + $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
1708 + wp_localize_script('vipps-admin', 'VippsConfig', $this->vippsJSConfig);
1561 1709 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1562 - $this->script_add_vippslocale();
1563 -
1564 - wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1710 + $this->script_add_vippslocale('vipps-admin');
1565 1711 wp_enqueue_script('vipps-admin');
1566 1712
1567 1713 wp_enqueue_style('vipps-admin-style',plugins_url('css/admin.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/admin.css"), 'all');
1568 1714 wp_enqueue_style('vipps-fonts');
@@ -1632,12 +1778,9 @@
1632 1778
1633 1779 public function wp_register_scripts () {
1634 1780 // We are going to use the 'hooks' library introduced by WP 5.1, but we still support WP 4.7. So if this isn't enqueues
1635 1781 // (which it only is if Gutenberg is active) or not provided at all, add it now.
1636 - if (!wp_script_is( 'wp-hooks', 'registered')) {
1637 - wp_register_script('wp-hooks', plugins_url('/compat/hooks.min.js', __FILE__));
1638 - }
1639 - wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/vipps.js"), 'true');
1782 + wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks', 'wp-api-fetch','vipps-widget-sdk'),filemtime(dirname(__FILE__) . "/js/vipps.js"), true);
1640 1783
1641 1784 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1642 1785 wp_register_script('vipps-onsite-messageing',
1643 1786 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
@@ -1643,11 +1786,25 @@
1643 1786 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1644 1787 array(),
1645 1788 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1646 1789 [
1647 - 'in_footer' => true,
1648 - 'strategy' => 'async',
1790 + 'in_footer' => true,
1791 + 'strategy' => 'async',
1649 1792 ],
1793 + );
1794 +
1795 + add_filter( 'script_loader_tag', function($tag, $handle,$src) {
1796 + if ($handle == 'vipps-widget-sdk') {
1797 + $tag = preg_replace("!^<script!", "<script data-vipps-widget-sdk ", $tag);
1798 + return $tag;
1799 + }
1800 + return $tag;
1801 + },10,3);
1802 +
1803 + wp_register_script('vipps-widget-sdk', "https://cdn.vippsmobilepay.com/js/widget-sdk/vipps-widget.js",
1804 + array('vipps-button-webcomponent'),
1805 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps.js'),
1806 + ['in_footer' => true]
1650 1807 );
1651 1808
1652 1809 // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1653 1810 wp_register_script('vipps-button-webcomponent',
@@ -1654,29 +1811,46 @@
1654 1811 plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1655 1812 array(),
1656 1813 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1657 1814 [
1658 - 'in_footer' => true,
1659 - 'strategy' => 'async',
1660 - ],
1815 + 'in_footer' => false
1816 + ]
1661 1817 );
1662 1818 }
1663 1819
1664 1820 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1665 - public function script_add_vippslocale () {
1821 + public function script_add_vippslocale ($handle) {
1666 1822 // This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13
1823 + $name = $this->get_payment_method_name();
1667 1824 $strings = array(
1668 - 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1669 - 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()),
1670 - 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1825 + 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $name),
1826 + 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $name),
1827 + 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $name),
1828 + 'termsAndConditionsError' => __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ),
1829 + 'temporaryError' => sprintf(__('%1$s is temporarily unavailable.','woo-vipps'),$name),
1830 + 'successMessage' => sprintf(__('To the %1$s app!','woo-vipps'), $name),
1831 + 'cancel'=> __("Cancel", 'woo-vipps'),
1832 + 'close'=> __("Close", 'woo-vipps'),
1833 + 'missingPaymentUrl'=> __("Successful checkout response has no payment URL", 'woo-vipps'),
1834 + 'expressCheckoutFailed'=> __("Express checkout failed", 'woo-vipps'),
1835 + 'unexpectedCheckoutResponse'=> __("Unexpected express checkout response", 'woo-vipps'),
1836 + 'vippsCheckoutFailed'=> __("Vipps Mobilepay checkout failed", 'woo-vipps'),
1837 + 'correctHighlightedFields'=> __("Please correct the highlighted fields.", 'woo-vipps'),
1838 + 'checkFormBeforeContinuing'=> __("Please check the form before continuing.", 'woo-vipps'),
1839 + 'cartCheckoutUnavailable'=> __("Cannot start express checkout: cart checkout is unavailable", 'woo-vipps'),
1840 + 'productIdentifiersMissing'=> __("Cannot buy product: product id, variation id and sku are missing", 'woo-vipps'),
1841 + 'productFormNotFound'=> __("Cannot buy product: product form not found", 'woo-vipps'),
1842 + 'paymentSuccessfulRedirecting' => __("Payment successful. Redirecting…", 'woo-vipps'),
1671 1843 );
1672 - wp_localize_script('vipps-gw', 'VippsLocale', $strings);
1844 + wp_localize_script($handle, 'VippsLocale', $strings);
1673 1845 }
1674 1846
1675 1847 public function wp_enqueue_scripts() {
1848 + // Add late: if this value isn't 'yes' we wil not add order attribution to express orders. IOK 2026-09-10
1849 + $this->vippsJSConfig['expressOrderAttribution'] = $this->gateway()->get_option('vippsorderattribution');
1676 1850 wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
1677 1851 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1678 - $this->script_add_vippslocale();
1852 + $this->script_add_vippslocale('vipps-gw');
1679 1853
1680 1854 wp_enqueue_script('vipps-gw');
1681 1855 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1682 1856 wp_enqueue_script('vipps-button-webcomponent');
@@ -1681,13 +1855,55 @@
1681 1855 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1682 1856 wp_enqueue_script('vipps-button-webcomponent');
1683 1857 }
1684 1858
1859 + // These scripts should be loaded only on the checkout screen and is used only for the classic shortcode checkout and
1860 + // the pay-for-order screen. IOK 2026-09-15
1861 + public function enqueue_classic_checkout_scripts () {
1862 + if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) {
1863 + return;
1864 + }
1865 + // Order-pay is rendered by the classic form even with a Blocks checkout page.
1866 + // It must bypass the check for the parent checkout page's block content.
1867 + if ( ! is_checkout_pay_page() ) {
1868 + $utils = '\\Automattic\\WooCommerce\\Blocks\\Utils\\CartCheckoutUtils';
1869 + $uses_checkout_block = is_callable( array( $utils, 'is_checkout_block_default' ) )
1870 + ? $utils::is_checkout_block_default()
1871 + : has_block( 'woocommerce/checkout', wc_get_page_id( 'checkout' ) );
1685 1872
1873 + if ( $uses_checkout_block ) {
1874 + return;
1875 + }
1876 + }
1877 +
1878 + // This script uses jQuery because the classic checkout screen does too. IOK 2026-09-15
1879 + $relative_path = 'js/vipps-classic-checkout.js';
1880 + wp_enqueue_script(
1881 + 'vipps-classic-checkout',
1882 + plugins_url( $relative_path, __FILE__ ),
1883 + array( 'jquery', 'wc-checkout', 'vipps-gw' ),
1884 + filemtime( plugin_dir_path( __FILE__ ) . $relative_path ),
1885 + true
1886 + );
1887 +
1888 + if ( is_checkout_pay_page() ) {
1889 + $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
1890 + wp_add_inline_script( 'vipps-classic-checkout', 'window.VippsOrderPayConfig = ' . wp_json_encode( array(
1891 + 'orderId' => $order ? $order->get_id() : 0,
1892 + 'orderKey' => $order ? $order->get_order_key() : '',
1893 + 'billingEmail' => $order ? $order->get_billing_email() : '',
1894 + 'endpoint' => $order ? rest_url( 'wc/store/v1/checkout/' . $order->get_id() ) : '',
1895 + 'nonce' => wp_create_nonce( 'wc_store_api' ),
1896 + 'billingAddress' => $order ? $order->get_address( 'billing' ) : array(),
1897 + 'shippingAddress' => $order ? $order->get_address( 'shipping' ) : array(),
1898 + ) ) . ';', 'before' );
1899 + }
1900 + }
1901 +
1902 +
1686 1903 public function add_shortcodes() {
1687 1904 add_shortcode('woo_vipps_buy_now', array($this, 'buy_now_button_shortcode'));
1688 1905 add_shortcode('woo_vipps_express_checkout_button', array($this, 'express_checkout_button_shortcode'));
1689 - add_shortcode('woo_vipps_express_checkout_banner', array($this, 'express_checkout_banner_shortcode'));
1690 1906
1691 1907 // Badges, if using shortcodes
1692 1908 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1693 1909 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
@@ -1692,8 +1908,11 @@
1692 1908 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1693 1909 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1694 1910 // Legacy vipps-badge shortcode. LP 19.11.2024
1695 1911 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1912 +
1913 + // special page handling, previously a fake page. LP 2026-08-25
1914 + add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1696 1915 }
1697 1916
1698 1917
1699 1918 public function log ($what,$type='info') {
@@ -1719,8 +1938,10 @@
1719 1938 }
1720 1939
1721 1940 // Show express button option on checkout form. LP 2026-03-23
1722 1941 public function checkout_before_customer_details_express () {
1942 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1943 + do_action('woo_vipps_checkout_before_customer_details_express');
1723 1944 $gw = $this->gateway();
1724 1945 if (!$gw->show_express_checkout()) return;
1725 1946 $this->express_checkout_section_html();
1726 1947 }
@@ -1728,58 +1949,14 @@
1728 1949 public function express_checkout_section_html() {
1729 1950 $payment_method = $this->get_payment_method_name();
1730 1951 $header_text = __('Express Checkout', 'woo-vipps');
1731 1952 $header = "<legend class='express-header'>$header_text</legend>";
1732 - $div_classes = "legacy-checkout vipps-express-checkout $payment_method";
1953 + $div_classes = "legacy-checkout express $payment_method";
1733 1954 echo "<fieldset class='$div_classes'>$header";
1734 1955 $this->checkout_express_checkout_button_html();
1735 1956 echo '</fieldset>';
1736 1957 }
1737 1958
1738 - public function express_checkout_banner() {
1739 - $gw = $this->gateway();
1740 - if (!$gw->show_express_checkout()) return;
1741 - return $this->express_checkout_banner_html();
1742 - }
1743 -
1744 - public function express_checkout_banner_html() {
1745 - $url = $this->express_checkout_url();
1746 - $url = wp_nonce_url($url,'express','sec');
1747 - $text = __('Skip entering your address and just checkout using', 'woo-vipps');
1748 - $linktext = 'Express'; // dont translate. LP 2025-09-03
1749 - $logo = $this->get_express_banner_logo();
1750 - $payment_method = $this->get_payment_method_name();
1751 -
1752 - $img_classes = 'express-banner-logo inline negative ' . strtolower($payment_method) . '-logo';
1753 - $div_classes = 'woocommerce-info ' . strtolower($payment_method) . '-info';
1754 - $a_classes = 'express-banner-link ' . strtolower($payment_method) . '-link';
1755 -
1756 - $message = $text . "<a href='$url' class='$a_classes'><img class='$img_classes' border=0 src='$logo' alt='$payment_method'/>$linktext!</a>";
1757 - $message = apply_filters('woo_vipps_express_checkout_banner', $message, $url, $payment_method);
1758 - ?>
1759 - <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1760 - <?php
1761 - }
1762 -
1763 - public function checkout_express_checkout_button() {
1764 - $gw = $this->gateway();
1765 -
1766 - if ($gw->show_express_checkout()){
1767 - return $this->checkout_express_checkout_button_html();
1768 - }
1769 - }
1770 -
1771 - public function checkout_express_checkout_button_html() {
1772 - $url = $this->express_checkout_url();
1773 - $url = wp_nonce_url($url,'express','sec');
1774 - $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1775 - $method = $this->get_payment_method_name();
1776 - $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1777 - $html = "<a href='$url' class='button vipps-express-checkout short $method' title='$title'>$button</a>";
1778 - $html = apply_filters('woo_vipps_cart_express_checkout_button', $button, $url);
1779 - echo $html;
1780 - }
1781 -
1782 1959 // Show the express button if reasonable to do so
1783 1960 public function cart_express_checkout_button() {
1784 1961 $gw = $this->gateway();
1785 1962
@@ -1791,24 +1968,42 @@
1791 1968 public function minicart_express_checkout_button() {
1792 1969 $gw = $this->gateway();
1793 1970
1794 1971 if ($gw->show_express_checkout()){
1795 - return $this->cart_express_checkout_button_html(true);
1972 + return $this->cart_express_checkout_button_html('minicart');
1796 1973 }
1797 1974 }
1798 1975
1799 - public function cart_express_checkout_button_html($minicart = false) {
1800 - $url = $this->express_checkout_url();
1801 - $url = wp_nonce_url($url,'express','sec');
1802 - $context = $minicart ? 'minicart' : 'cart';
1803 - $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1976 + // This is for the Vipps SDK button used instead of the normal "pay for order" and "confirm order" buttons
1977 + // on the classic checkout and pay-for-order pages. It gets swapped in when the user selects vipps, and swapped out otherwise.
1978 + public function add_checkout_button_for_classic () {
1979 + $button = $this->get_html_button_for_context('checkout');
1980 + $submit = "<div class='vipps-classic-checkout-container'><button id='vipps-classic-checkout-submit' class='hidden vipps-submit-wrapper' type='submit'>$button</button></div>";
1981 + echo $submit;
1982 + }
1983 +
1984 + public function cart_express_checkout_button_html($context= 'cart') {
1985 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1804 1986 $method = $this->get_payment_method_name();
1805 1987 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1806 - $html = "<a href='$url' class='button vipps-express-checkout short $method' title='$title'>$button</a>";
1807 - $html = apply_filters('woo_vipps_cart_express_checkout_button', $button, $url);
1988 + $url = "#";
1989 + $sec = wp_create_nonce('express');
1990 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
1991 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1808 1992 echo $html;
1809 1993 }
1810 1994
1995 + public function checkout_express_checkout_button_html() {
1996 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1997 + $method = $this->get_payment_method_name();
1998 + $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1999 + $url = "#";
2000 + $sec = wp_create_nonce('express');
2001 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
2002 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
2003 + echo $html;
2004 + }
2005 +
1811 2006 // A shortcode for a single buy now button. Express checkout must be active; but I don't check for this here, as this button may be
1812 2007 // cached. Therefore stock, purchasability etc will be done later. IOK 2018-10-02
1813 2008 public function buy_now_button_shortcode ($atts) {
1814 2009 // The new web component button args. LP 2026-07-02
@@ -1843,19 +2038,11 @@
1843 2038 public function express_checkout_button_shortcode() {
1844 2039 $gw = $this->gateway();
1845 2040 if (!$gw->cart_supports_express_checkout()) return;
1846 2041 ob_start();
1847 - $this->cart_express_checkout_button_html('shortcode');
2042 + $this->cart_express_checkout_button_html('cart');
1848 2043 return ob_get_clean();
1849 2044 }
1850 - // Show a banner normally shown for non-logged-in-users at the checkout page. It does not need to check if we are to show the button, obviously, but needs to see if the cart works
1851 - public function express_checkout_banner_shortcode() {
1852 - $gw = $this->gateway();
1853 - if (!$gw->cart_supports_express_checkout()) return;
1854 - ob_start();
1855 - $this->express_checkout_banner_html();
1856 - return ob_get_clean();
1857 - }
1858 2045
1859 2046 // Manage the various product meta fields
1860 2047 public function process_product_meta ($id, $post) {
1861 2048 // This is for the 'buy now' button
@@ -2381,82 +2568,9 @@
2381 2568 if ($ok) return $callbackdir;
2382 2569 return null;
2383 2570 }
2384 2571
2385 - // Unfortunately, we cannot do any form of portable locking, and we may get callbacks from Vipps arriving at the same moment as we check the status at Vipps,
2386 - // which in the very worst case, for Express Checkout orders, may lead to a double shipping line. Changing this to a queue system is non-trivial, because some of
2387 - // the operations done when modifying the order actually requires the customers session to be active. This operation will make conflicts a litte less probable
2388 - // by implementing something that isn't quite a lock, and the filter may be used to implement proper locking, using e.g. flock, where this can be used
2389 - // (non-distributed environments using unix on standard filesystems. IOK 2020-05-15
2390 - // Returns true if lock succeeds, or false.
2391 - public function lockOrder($order) {
2392 - $orderid = $order->get_id();
2393 - if (has_filter('woo_vipps_lock_order')) {
2394 - $ok = apply_filters('woo_vipps_lock_order', $order);
2395 - if (!$ok) return false;
2396 - } else {
2397 - if(get_transient('order_lock_'.$orderid)) return false;
2398 - $this->lockKey = uniqid();
2399 - set_transient('order_lock_' . $orderid, $this->lockKey, 30);
2400 - }
2401 - add_action('shutdown', function () use ($order) { global $Vipps; $Vipps->unlockOrder($order); });
2402 - return true;
2403 - }
2404 - // If the order is locked, it means it is in the process of being finalized, so for instance, we do *not* want to abandon it
2405 - // in checkout.
2406 - public function isLocked ($order) {
2407 - $orderid = $order->get_id();
2408 - $locked = get_transient('order_lock_'.$orderid);
2409 - return apply_filters('woo_vipps_order_locked', $locked, $order);
2410 - }
2411 - public function unlockOrder($order) {
2412 - $orderid = $order->get_id();
2413 - if (has_action('woo_vipps_unlock_order')) {
2414 - do_action('woo_vipps_unlock_order', $order);
2415 - } else {
2416 - if(get_transient('order_lock_'.$orderid) == $this->lockKey) {
2417 - delete_transient('order_lock_'.$orderid);
2418 - }
2419 - }
2420 - }
2421 2572
2422 - // Functions using flock() and files to lock orders. This is only guaranteed to work on certain setups, ie, non-distributed setups
2423 - // using Unix with normal filesystems (not NFS).
2424 - public function flock_lock_order($order) {
2425 - global $_orderlocks;
2426 - if (!$_orderlocks) $_orderlocks = array();
2427 - $dir = $this->callbackDir();
2428 - if (!$dir) {
2429 - $this->log(__("Cannot use flock() to lock orders: cannot create or write to directory", "woo-vipps"), 'error');
2430 - return true;
2431 - }
2432 - $fname = '.ht-vipps-lock-'.md5($order->get_order_key() . $order->get_meta('_vipps_transaction'));
2433 - $path = $dir . DIRECTORY_SEPARATOR . $fname;
2434 - touch($path);
2435 - if (!is_writable($path)) {
2436 - $this->log(__("Cannot use flock() to lock orders: cannot create lockfiles ", "woo-vipps"), 'error');
2437 - return true;
2438 - }
2439 - $handle = fopen($path, 'w+');
2440 - if (flock($handle, LOCK_EX | LOCK_NB)) {
2441 - $_orderlocks[$order->get_id()] = array($handle,$path);
2442 - return true;
2443 - }
2444 - return false;
2445 - }
2446 - public function flock_unlock_order($order) {
2447 - $orderid=$order->get_id();
2448 - global $_orderlocks;
2449 - if (!$_orderlocks) return;
2450 - if (!isset($_orderlocks[$orderid])) return;
2451 - list($handle, $path) = $_orderlocks[$orderid];
2452 - unset($_orderlocks[$orderid]);
2453 - flock($handle, LOCK_UN);
2454 - fclose($handle);
2455 - @unlink($path);
2456 - }
2457 -
2458 -
2459 2573 // Because the prefix used to create the Vipps order id is editable
2460 2574 // by the user, we will store that as a meta and use this for callbacks etc.
2461 2575 // IOK 2023-01-23 this function is no longer used, and kept only for backwards compatibility with
2462 2576 // debug filters and similar.
@@ -2506,77 +2620,93 @@
2506 2620 return null;
2507 2621 }
2508 2622 }
2509 2623
2624 + // Special pages, and some callbacks. IOK 2018-05-18
2625 + public function template_redirect() {
2510 2626
2511 - // If this is a special page, return true very early because we are handling this. IOK 2023-02-22
2512 - public function pre_handle_404($current, $query) {
2513 - if (!is_admin()) {
2514 - $special = $this->is_special_page();
2515 - if ($special) {
2516 - // Ensure very early on that Autooptimize does not try to optimize us (if installed) IOK 2023-03-04
2517 - add_filter( 'autoptimize_filter_noptimize', '__return_true');
2518 - return true;
2519 - }
2627 + // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2628 + // We redirect these to the new location.
2629 + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2630 + if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2631 + $url = static::get_special_page_url();
2632 + $_GET['action'] = 'buy_product';
2633 + $q = build_query($_GET);
2634 + wp_redirect($url . "?" . $q, 302);
2635 + exit();
2520 2636 }
2521 - return $current;
2637 +
2638 + if (static::is_special_page()) {
2639 + // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2640 + remove_filter('template_redirect', 'redirect_canonical', 10);
2641 + // dont cache special page. LP 2026-08-25
2642 + $this->nocache();
2643 + // Do the custom pre-load actions for these pages IOK 2026-09-11
2644 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2645 + }
2522 2646 }
2523 2647
2524 - // Special pages, and some callbacks. IOK 2018-05-18
2525 - public function template_redirect() {
2526 - global $post;
2527 - // Handle special callbacks
2528 - $special = $this->is_special_page() ;
2648 + // Ran in template redirect for the special page. IOK 2026-09-2
2649 + public function pre_special_page_actions ($action) {
2650 + // Change title dynamically depending on action. LP 2026-09-02
2651 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2529 2652
2530 - if ($special) {
2531 - remove_filter('template_redirect', 'redirect_canonical', 10);
2532 - do_action('woo_vipps_before_handling_special_page', $special);
2653 + // If we are handling the 'wait for payment' action, we need to poll the order status before
2654 + // we start producing content IOK 2026-09-21
2655 + if ($action == 'wait_for_payment') {
2656 + $this->handle_payment_poll_and_redirect();
2657 + }
2533 2658
2534 - // Allow above hook to actually handle special pages. It should probably call $Vipps->fakepage or a redirect; can be used
2535 - // to intercept express checkout etc. IOK 2022-03-18
2536 - if (! apply_filters('woo_vipps_special_page_handled', false, $special)) {
2537 - $this->$special();
2538 - }
2659 + // Some validation is required for this action
2660 + if ($action == 'do_express_checkout') {
2661 + $this->vipps_express_checkout_consistency_check();
2539 2662 }
2663 + // These two actions require an extra script
2664 + if (in_array($action, ['buy_product','do_express_checkout'])) {
2665 + wp_enqueue_script('vipps-purchase', plugins_url('js/vipps-purchase.js',__FILE__), ['vipps-gw'],
2666 + filemtime(dirname(__FILE__) . "/js/vipps-purchase.js"),
2667 + ['in_footer'=>true]
2668 + );
2669 + }
2670 + }
2540 2671
2541 - $consentremoval = $this->is_consent_removal();
2542 - if ($consentremoval) {
2543 - remove_filter('template_redirect', 'redirect_canonical', 10);
2544 - do_action('woo_vipps_before_handling_special_page', 'consentremoval');
2545 - if (! apply_filters('woo_vipps_special_page_handled', false, 'consentremoval')) {
2546 - $this->vipps_consent_removal_callback($consentremoval);
2672 + // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2673 + public function vipps_special_page_endpoint_title($title, $postid = 0) {
2674 + global $wp_query;
2675 + // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2676 +
2677 + // In block themes the whole template (header, footer, content) renders inside the main
2678 + // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2679 + // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2680 + // the queried page so an earlier title doesn't consume this one-shot filter.
2681 + if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2682 + switch ($_GET['action'] ?? '') {
2683 + case 'wait_for_payment':
2684 + $title = __('Processing order', 'woo-vipps');
2685 + break;
2686 + case 'do_express_checkout':
2687 + case 'buy_product':
2688 + $title = __('Express Checkout', 'woo-vipps');
2689 + break;
2547 2690 }
2548 2691 }
2692 + return $title;
2549 2693 }
2694 +
2550 2695 // Template handling for special pages. IOK 2018-11-21
2696 + // This is legacy - the special page is now a real page, so it can have a special template using standard WP methods. IOK 2026-09-11
2551 2697 public function template_include($template) {
2552 - $special = $this->is_special_page() ;
2553 - if ($special) {
2698 + if (static::is_special_page()) {
2554 2699 // Get any special template override from the options IOK 2020-02-18
2555 2700 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2556 2701 $found = locate_template($specific,false,false);
2557 2702 if ($found) $template=$found;
2558 2703
2559 - return apply_filters('woo_vipps_special_page_template', $template, $special);
2704 + return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2560 2705 }
2561 2706 return $template;
2562 2707 }
2563 2708
2564 -
2565 - // Can't use wc-api for this, as that does not support DELETE . IOK 2018-05-18
2566 - private function is_consent_removal () {
2567 -
2568 - if ($_SERVER['REQUEST_METHOD'] != 'DELETE') return false;
2569 - if ( !get_option('permalink_structure')) {
2570 - if (@$_REQUEST['vipps-consent-removal']) return @$_REQUEST['callback'];
2571 - return false;
2572 - }
2573 - if (preg_match("!/vipps-consent-removal/([^/]*)!", $_SERVER['REQUEST_URI'], $matches)) {
2574 - return @$_REQUEST['callback'];
2575 - }
2576 - return false;
2577 - }
2578 -
2579 2709 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2580 2710 // the user if using Express Checkout IOK 2020-10-09
2581 2711 public function woocommerce_before_thankyou ($orderid) {
2582 2712 $order = wc_get_order($orderid);
@@ -2581,9 +2711,9 @@
2581 2711 public function woocommerce_before_thankyou ($orderid) {
2582 2712 $order = wc_get_order($orderid);
2583 2713 if ($order) {
2584 2714 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2585 - // -- or the same thing for Vipps Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2715 + // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2586 2716 $this->maybe_log_in_user($order);
2587 2717 $order->delete_meta_data('_vipps_limited_session');
2588 2718 $order->save();
2589 2719
@@ -2644,9 +2774,8 @@
2644 2774
2645 2775 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2646 2776 // IOK 2025-11-19
2647 2777 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2648 -
2649 2778 }
2650 2779
2651 2780 public function get_payment_method_name() {
2652 2781 return $this->gateway()->get_option('payment_method_name');
@@ -2666,14 +2795,13 @@
2666 2795 public function after_setup_theme() {
2667 2796 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2668 2797 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2669 2798
2670 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2799 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2671 2800 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2672 2801 // is important.
2673 2802 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2674 2803
2675 -
2676 2804 // Callbacks use the Woo API IOK 2018-05-18
2677 2805 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2678 2806 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2679 2807
@@ -2684,19 +2812,21 @@
2684 2812 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2685 2813 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2686 2814
2687 2815 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2688 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2816 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2689 2817 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2690 2818
2691 2819 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2692 2820 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2693 2821
2822 + // For the classic checkout page and pay-for-order page, use a custom submit button when payment method
2823 + // is Vipps
2824 + add_action('woocommerce_review_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2825 + add_action('woocommerce_pay_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2694 2826
2695 - // Special pages and callbacks handled by template_redirect
2696 - // We must also notify WP and other plugins that we are handling this 404-like situation. IOK 2023-02-22
2827 + // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2697 2828 add_action('template_redirect', array($this,'template_redirect'),1);
2698 - add_action('pre_handle_404', array($this, 'pre_handle_404'), 1, 2);
2699 2829
2700 2830 // Allow overriding their templates
2701 2831 add_filter('template_include', array($this,'template_include'), 10, 1);
2702 2832
@@ -2703,21 +2833,8 @@
2703 2833 // Ajax endpoints for checking the order status while waiting for confirmation
2704 2834 add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status'));
2705 2835 add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status'));
2706 2836
2707 -
2708 - // Buying a single product directly using express checkout IOK 2018-09-28
2709 - add_action('wp_ajax_nopriv_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2710 - add_action('wp_ajax_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2711 -
2712 - // This is for express checkout which we will also do asynchronously IOK 2018-05-28
2713 - add_action('wp_ajax_nopriv_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2714 - add_action('wp_ajax_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2715 -
2716 - // Same thing, but for single products IOK 2018-05-28
2717 - add_action('wp_ajax_nopriv_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2718 - add_action('wp_ajax_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2719 -
2720 2837 // Handle the cancel unpaid order action when the "hold stock" times out.
2721 2838 // For *normal* vipps orders, we run another cronjob every 5. minute which checks order status,
2722 2839 // therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return.
2723 2840 // For Checkout the rules are different though.
@@ -2796,9 +2913,8 @@
2796 2913 $this->vippsJSConfig = array();
2797 2914 $this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php');
2798 2915 $this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps');
2799 2916 $this->vippsJSConfig['BuyNowWithVipps'] = sprintf(__('Buy now with %1$s', 'woo-vipps'), $this->get_payment_method_name());
2800 - $this->vippsJSConfig['vippslogourl'] = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
2801 2917 $this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__);
2802 2918 $this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name());
2803 2919 $this->vippsJSConfig['vippsbuynowdescription'] = sprintf(__( 'Add a %1$s Buy Now-button to the product block or choose a product manually', 'woo-vipps'), $this->get_payment_method_name());
2804 2920 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
@@ -2803,10 +2919,17 @@
2803 2919 $this->vippsJSConfig['vippsbuynowdescription'] = sprintf(__( 'Add a %1$s Buy Now-button to the product block or choose a product manually', 'woo-vipps'), $this->get_payment_method_name());
2804 2920 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2805 2921 $this->vippsJSConfig['vippslocale'] = get_locale();
2806 2922 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
2807 -
2923 + $this->vippsJSConfig['paymentMethodSlug'] = sanitize_title($this->get_payment_method_name());
2924 + $this->vippsJSConfig['paymentMethodName'] = $this->get_payment_method_name();
2925 + $wc_lang = $this->get_html_button_attrs_for_context()['language'];
2926 + if ('store' === $wc_lang) $wc_lang = $this->get_customer_language();
2927 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
2928 + if ('dk' === $wc_lang) $wc_lang = 'da';
2929 + $this->vippsJSConfig['webcomponentLanguage'] = $wc_lang;
2808 2930
2931 +
2809 2932 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2810 2933 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
2811 2934 // Ensure gateways are loaded at this point IOK 2026-05-27
2812 2935 require_once(dirname(__FILE__) . '/WC_Gateway_VippsCard.class.php');
@@ -2982,14 +3105,14 @@
2982 3105
2983 3106 $raw_post = @file_get_contents( 'php://input' );
2984 3107 $result = @json_decode($raw_post,true);
2985 3108
2986 - // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
3109 + // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
2987 3110 // .. and the epayment webhooks 2023-12-19
2988 3111 $ischeckout = false;
2989 3112 $iswebhook = false;
2990 3113 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
2991 - // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
3114 + // For Checkout v3 and onwards, we control the callback so the type is just this field
2992 3115 if ($callback == 'checkout') {
2993 3116 $ischeckout = true;
2994 3117 }
2995 3118 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
@@ -3403,10 +3526,10 @@
3403 3526 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3404 3527 exit();
3405 3528 }
3406 3529
3407 - // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3408 - // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3530 + // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3531 + // special support for Checkout that this is in fact happening. IOK 2023-01-19
3409 3532 // This needs to be done before "calculate totals".
3410 3533 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3411 3534 $ischeckout = $order->get_meta('_vipps_checkout');
3412 3535
@@ -3582,9 +3705,9 @@
3582 3705 ), 'debug');
3583 3706
3584 3707 }
3585 3708
3586 - // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3709 + // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3587 3710 // 'dynamic pricing' 2023-01-26
3588 3711 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3589 3712 $taxrate = 0;
3590 3713 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
@@ -3678,9 +3801,9 @@
3678 3801 $shipping_method = $methodclass ? new $methodclass($rate->get_instance_id()) : null;
3679 3802
3680 3803 $tax = $rate->get_shipping_tax() ?: 0;
3681 3804 $cost = $rate->get_cost() ?: 0;
3682 - $label = $rate->get_label();
3805 + $label = html_entity_decode($rate->get_label());
3683 3806
3684 3807 if ($cost == 0 && ($methodid != 'local_pickup' && $methodid != 'pickup_location')) {
3685 3808 $has_free_shipping = true;
3686 3809 }
@@ -3706,13 +3829,13 @@
3706 3829 $shippingcostB = sprintf("%.2F",wc_format_decimal($cost, '') + wc_format_decimal($tax,''));
3707 3830 $shippingcost = max($shippingcostA, $shippingcostB);
3708 3831
3709 3832 $vippsmethod['shippingCost'] = $shippingcost;
3710 - $vippsmethod['shippingMethod'] = $rate->get_label();
3833 + $vippsmethod['shippingMethod'] = html_entity_decode($rate->get_label());
3711 3834 $vippsmethod['shippingMethodId'] = $key;
3712 3835 $vippsmethods[]=$vippsmethod;
3713 3836
3714 - // Metadata and settings stored for later use for Vipps Checkout
3837 + // Metadata and settings stored for later use for Checkout
3715 3838 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3716 3839 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3717 3840 // finalize the order. IOK 2025-08-15
3718 3841 $ratemap[$key]=$rate;
@@ -3730,9 +3853,9 @@
3730 3853 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3731 3854 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3732 3855 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3733 3856
3734 - // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3857 + // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3735 3858 // IOK 2025-08-15 and new Express yet another slightly different format.
3736 3859 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3737 3860 if ($ischeckout) {
3738 3861 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
@@ -4022,9 +4145,9 @@
4022 4145 WC()->cart->calculate_totals();
4023 4146 WC()->cart->set_session();
4024 4147 return true;
4025 4148 } catch (Exception $e) {
4026 - $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error');
4149 + $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->getMessage()), 'error');
4027 4150 return false;
4028 4151 }
4029 4152 }
4030 4153
@@ -4053,9 +4176,9 @@
4053 4176 $tax = $rate->get_shipping_tax() ?: 0;
4054 4177 $cost = $rate->get_cost() ?: 0;
4055 4178
4056 4179 $method['shippingCost'] = sprintf("%.2F",wc_format_decimal($cost+$tax,''));
4057 - $method['shippingMethod'] = $rate->get_label();
4180 + $method['shippingMethod'] = html_entity_decode($rate->get_label());
4058 4181 // We may not really need the tax stashed here, but just to be sure.
4059 4182 $method['shippingMethodId'] = $rate->get_id() . ";" . $tax;
4060 4183 $methods[]= $method;
4061 4184
@@ -4108,17 +4231,8 @@
4108 4231 header("X-Accel-Expires: 0");
4109 4232 }
4110 4233
4111 4234
4112 -
4113 - // Handle DELETE on a vipps consent removal callback
4114 - public function vipps_consent_removal_callback ($callback) {
4115 - Vipps::nocache();
4116 - // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
4117 - // to escape here when the API is changed. IOK 2020-10-14
4118 - $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
4119 - }
4120 -
4121 4235 public function woocommerce_payment_gateways($methods) {
4122 4236 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4123 4237 require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
4124 4238 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
@@ -4143,9 +4257,11 @@
4143 4257 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
4144 4258 return $url;
4145 4259 }
4146 4260 $url = $this->express_checkout_url();
4147 - $url = wp_nonce_url($url,'express','sec');
4261 + // At this point, there is always a query argument here. IOK 2026-09-21
4262 + $nonce = wp_create_nonce('express');
4263 + $url = $url . "&sec=$nonce";
4148 4264
4149 4265 return $url;
4150 4266 }
4151 4267
@@ -4212,9 +4328,9 @@
4212 4328 // Poll status and correct woo status. LP 2026-05-19
4213 4329 $order_data = $gw->get_payment_details($order);
4214 4330
4215 4331 // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4216 - if ('CANCEL' === $order_data['STATE']) {
4332 + if ('CANCEL' === ($order_data['state'] ?? "")) {
4217 4333 /* translators: company name */
4218 4334 $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4219 4335 return;
4220 4336 }
@@ -4241,20 +4357,40 @@
4241 4357 }
4242 4358 }
4243 4359
4244 4360 public function activate () {
4245 - static::maybe_add_cron_event();
4246 - $gw = $this->gateway();
4361 + static::maybe_add_cron_event();
4362 + $gw = $this->gateway();
4247 4363
4248 - // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4249 - if ($gw->get_option('orderprefix') == 'Woo') {
4250 - $gw->update_option('orderprefix', $this->generate_order_prefix());
4251 - }
4252 - // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4253 - $gw->initialize_webhooks();
4254 - $this->payment_method_name = $gw->get_option('payment_method_name');
4255 - }
4364 + // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4365 + if ($gw->get_option('orderprefix') == 'Woo') {
4366 + $gw->update_option('orderprefix', $this->generate_order_prefix());
4367 + }
4368 + // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4369 + $gw->initialize_webhooks();
4370 + $this->payment_method_name = $gw->get_option('payment_method_name');
4256 4371
4372 +
4373 + // Check if the special page is noted and actually does exist
4374 + $special = static::get_special_page_id();
4375 + if ($special) {
4376 + $special_page = get_post($special);
4377 + if ($special_page && 'trash' !== $special_page->post_status) {
4378 + // Ensure this page has the necessary shortcode. LP 2026-09-01
4379 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4380 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4381 + wp_update_post([
4382 + 'ID' => $special,
4383 + 'post_content' => $new_content,
4384 + ]);
4385 + }
4386 + } else {
4387 + delete_option('woocommerce_vipps_special_page_page_id');
4388 + }
4389 + }
4390 +
4391 + }
4392 +
4257 4393 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4258 4394 public static function deactivate() {
4259 4395 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4260 4396 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
@@ -4307,9 +4443,10 @@
4307 4443 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4308 4444 private function maybe_set_vipps_as_default() {
4309 4445 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4310 4446 $gw = $this->gateway();
4311 - if ($gw->get_option('vippsdefault')=='yes') {
4447 + // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4448 + if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4312 4449 WC()->session->set('chosen_payment_method', $gw->id);
4313 4450 }
4314 4451 }
4315 4452
@@ -4321,28 +4458,12 @@
4321 4458 $order = wc_get_order($order->get_id());
4322 4459 $order_status = $order->get_status();
4323 4460
4324 4461 if ($order_status != 'pending') return $order_status;
4325 - // No callback has occured yet. If this has been going on for a while, check directly with Vipps
4326 - // We can't use the vipps init timestamp here, because that may be in the past for Checkout at least. IOK 2025-08-13
4327 - if ($order_status == 'pending') {
4328 - if (WC()->session) {
4329 - $now = time();
4330 - $then = WC()->session->get('_vipps_check_' . $order->get_id());
4331 - if (!$then) {
4332 - $then = $now;
4333 - WC()->session->set('_vipps_check_' . $order->get_id(), $then);
4334 - }
4335 - if (($then + (1 * 30)) > $now) { // more than half a minute? Start checking at Vipps
4336 - return $order_status;
4337 - }
4338 - } else {
4339 - // No session shouldn't be possible, but if it is..
4340 - return $order_status;
4341 - }
4342 - }
4462 +
4463 + $gw = $this->gateway();
4343 4464 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4344 - return $this->check_status_of_pending_order($order);
4465 + $newstatus = $gw->poll_and_check_order_status($order);
4345 4466 }
4346 4467
4347 4468 // In some situations we have to empty the cart when the user goes to Vipps, so
4348 4469 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
@@ -4410,17 +4531,18 @@
4410 4531
4411 4532 // Maybe log in user
4412 4533 // It is done on the thank-you page of the order, and only for express checkout.
4413 4534 function maybe_log_in_user ($order) {
4535 +
4414 4536 if (is_user_logged_in()) return;
4415 4537 if (!$order || ! self::is_vipps_order($order)) return;
4416 4538
4417 4539 // We *do* want to log in express checkout customers, but not those that
4418 - // use the Vipps Checkout solution - those can change their emails in the
4540 + // use the Checkout solution - those can change their emails in the
4419 4541 // checkout screen. IOK 2021-09-03
4420 4542 $do_login = $order->get_meta('_vipps_express_checkout');
4421 4543
4422 - // We will not log in Vipps Checkout users unless the option for that is true
4544 + // We will not log in Checkout users unless the option for that is true
4423 4545 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4424 4546 $do_login = false;
4425 4547 }
4426 4548
@@ -4455,9 +4577,9 @@
4455 4577
4456 4578 // Both Checkout and Express Checkout have the below value set to true
4457 4579 if (!$order->get_meta('_vipps_express_checkout')) return;
4458 4580
4459 - // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4581 + // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4460 4582 // IOK 2023-07-27
4461 4583 $ischeckout = $order->get_meta('_vipps_checkout');
4462 4584 if ($ischeckout) {
4463 4585 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
@@ -4557,129 +4679,233 @@
4557 4679 }
4558 4680 if (!$o) return;
4559 4681 if (!$o->get_meta('_vipps_single_product_express')) return;
4560 4682 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4561 - if ($failed) WC()->cart->empty_cart();
4683 + // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4684 + WC()->cart->empty_cart();
4562 4685 $this->restore_cart($o);
4563 4686 }
4564 4687
4565 4688
4566 - public function ajax_vipps_buy_single_product () {
4567 - Vipps::nocache();
4568 - static::set_locale_if_in_header();
4569 - // We're not checking ajax referer here, because what we do is creating a session and redirecting to the
4570 - // 'create order' page wherein we'll do the actual work. IOK 2018-09-28
4571 - $session = WC()->session;
4572 - if (!$session->has_session()) {
4573 - $session->set_customer_session_cookie(true);
4689 + // Actually create a express checkout order object, with no shipping or personal information, returning information about
4690 + // the result. The order should at this point be in a/the cart. For single product purchases, this is a different cart than
4691 + // the main one; for cart purchases, it's just the WC()->cart object. IOK 2026-08-25
4692 + private function create_and_process_express_order() {
4693 + $result = null;
4694 + $gw = $this->gateway();
4695 + try {
4696 + $orderid = $gw->create_partial_order();
4697 + do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4698 + } catch (Exception $e) {
4699 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4700 + return $result;
4701 + }
4702 + if (!$orderid) {
4703 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4704 + return $result;
4574 4705 }
4575 - $session->set('__vipps_buy_product', json_encode($_REQUEST));
4576 4706
4577 - // Incredibly, some caches will cache this page even with cookies set and no-cache headers set. So we try to
4578 - // add yet another way to inform caches that this is, in fact, not cacheable. IOK 2023-06-12
4579 - $url = add_query_arg('nc', sha1(uniqid(WC()->session->get_customer_id(),true)), $this->buy_product_url());
4707 + try {
4708 + $this->maybe_add_static_shipping($gw,$orderid);
4709 + } catch (Exception $e) {
4710 + $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4711 + $this->log($e->getMessage(),'error');
4712 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4713 + return $result;
4714 + }
4580 4715
4581 - $result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url);
4582 - wp_send_json($result);
4583 - exit();
4716 + // Now pass this to the Woo gateway and get a redirect URL back IOK 2026-08-25
4717 + $ok = $gw->process_payment($orderid);
4718 + if ($ok && $ok['result'] == 'success') {
4719 + $result = array('ok'=>1, 'orderid'=>$orderid, 'msg'=>'', 'url'=>$ok['redirect']);
4720 + return $result;
4721 + }
4722 + $result = array('ok'=>0, 'orderid'=>$orderid, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4723 + return $result;
4584 4724 }
4585 4725
4586 - public function ajax_do_express_checkout () {
4587 - check_ajax_referer('do_express','sec');
4588 - Vipps::nocache();
4589 - static::set_locale_if_in_header();
4726 + // This creates a simple hash for the 'current order' which we will store in the session if we proceed to checkout. We use this to
4727 + // avoid/warn the user of duplicate purchases. IOK 2026-09-09
4728 + public function create_order_hash($args=null) {
4729 + // If we have no arguments, we'll hash the cart.
4730 + if (empty($args)) {
4731 + $cartitems = WC()->cart->get_cart();
4732 + $orderspec = array();
4733 + foreach($cartitems as $item => $values) {
4734 + $orderspec[] = array('sku'=> ($values['sku'] ?? ""), 'product_id'=>($values['product_id'] ?? 0), 'variation_id'=>($values['variation_id'] ?? 0), 'quantity'=>($values['quantity'] ?? 1));
4735 + }
4736 + $args = $orderspec;
4737 + }
4738 + return md5(serialize($args));
4739 + }
4740 +
4741 +
4742 + // This method may provide HTML form elements to ask a user questions after starting
4743 + // express checkout. It is used to detect duplicate orders, possibly for terms and conditions, and user-definiable customizations. IOK 2026-09-09
4744 + // NULL productinfo means use the cart; the "current hash" is used to detect duplicates, and is calculated by the caller.
4745 + public function express_order_needs_confirmation($args, $productinfo, $current_hash) {
4746 + $elements = [];
4747 + $html = "";
4748 +
4749 + // First, let's check if we need to confirm the purchase.
4750 + $last_express_purchase_hash = WC()->session->get('woo_vipps_last_express');
4751 + if ($last_express_purchase_hash) {
4752 + list($hash, $orderid, $stamp) = explode(":", $last_express_purchase_hash);
4753 + $cutoff = $stamp + apply_filters('woo_vipps_recent_order_cutoff', (3*60));
4754 + if ($hash == $current_hash && (time() <= $cutoff )) {
4755 + $order = wc_get_order($orderid);
4756 + $status = $order ? $order->get_status() : false;
4757 + // IOK TODO/FIXME actually, if the order is pending/failed/cancelled and *identical* to our current productinfo, we could plausibly do a restart here. Would probably require careful checking though, and
4758 + // a different flow. IOK 2026-09-17
4759 + if (in_array($status, ['on-hold', 'processing', 'completed'])) {
4760 + $header = __("Are you sure?",'woo-vipps');
4761 + $body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps');
4762 + $elements['possible_duplicate'] = "<h1>$header</h1><p>$body</p>";
4763 + $this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info');
4764 + }
4765 + }
4766 + }
4767 +
4590 4768 $gw = $this->gateway();
4769 + $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
4770 + $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
4771 + $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
4591 4772
4592 - if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4593 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4594 - wp_send_json($result);
4595 - exit();
4773 + if ($askForTerms) {
4774 + $termsHTML = '';
4775 + // Include shop terms
4776 + ob_start();
4777 + wc_get_template('checkout/terms.php');
4778 + $termsHTML = ob_get_clean();
4779 + $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
4780 + $elements['terms'] = $termsHTML;
4596 4781 }
4597 4782
4783 + // Custom fields
4784 + ob_start();
4785 + do_action('woo_vipps_express_checkout_orderspec_form', $productinfo, $args);
4786 + $extra_fields = ob_get_clean();
4787 + if (!empty($extra_fields)) {
4788 + $elements['extra'] = $extra_fields;
4789 + }
4598 4790
4599 -
4791 + if (!empty($elements)) {
4792 + $html = join("\n", array_values($elements));
4793 + $msg = join(",", array_keys($elements));
4794 + return ['ok'=>2, 'msg'=>$msg, 'html'=>$html, 'url'=>''];
4795 + }
4600 4796
4797 + return false;
4798 +
4799 + }
4800 +
4801 + public function rest_do_express_checkout ($request) {
4802 + Vipps::nocache();
4803 + check_ajax_referer('express', 'sec');
4804 + static::set_locale_if_in_header();
4805 + $args = $request->get_json_params();
4806 + if (!$args) {
4807 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4808 + }
4809 +
4810 + // Since this is the REST api, we need to load the cart manually here. IOK 2026-08-27
4811 + if ( is_null( WC()->cart ) ) {
4812 + WC()->frontend_includes();
4813 + if ( ! WC()->session instanceof WC_Session ) {
4814 + WC()->session = new WC_Session_Handler();
4815 + WC()->session->init();
4816 + }
4817 + if (is_null( WC()->customer)) {
4818 + WC()->customer = new WC_Customer( get_current_user_id(), true );
4819 + }
4820 + WC()->cart = new WC_Cart();
4821 + WC()->cart->get_cart_from_session();
4822 + }
4823 +
4824 +
4825 + $gw = $this->gateway();
4826 + if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4827 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4828 + return $result;
4829 + }
4601 4830 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4602 4831 $toolate = false;
4603 4832 $msg = "";
4604 4833 $valid = WC()->cart->check_cart_item_validity();
4605 4834 if ( is_wp_error( $valid) ) {
4606 - $toolate = true;
4607 - $msg = "<br>" . $valid->get_error_message();
4835 + $toolate = true;
4836 + $msg = "<br>" . $valid->get_error_message();
4608 4837 }
4609 4838 $stock = WC()->cart->check_cart_item_stock();
4610 - if ( is_wp_error( $stock) ) {
4611 - $toolate = true;
4612 - $msg = "<br>" . $stock->get_error_message();
4613 - }
4839 + if ( is_wp_error( $stock) ) {
4840 + $toolate = true;
4841 + $msg = "<br>" . $stock->get_error_message();
4842 + }
4614 4843
4615 4844 if ($toolate) {
4616 4845 $result = array('ok'=>0, 'msg'=>sprintf(__('Some of the products in your cart are no longer available in the quantities you have ordered. Please <a href="%1$s">edit your order</a> before continuing the checkout','woo-vipps'), wc_get_cart_url()) . $msg, 'url'=>false);
4617 - wp_send_json($result);
4618 - exit();
4846 + return $result;
4619 4847 }
4620 4848
4621 - try {
4622 - $orderid = $gw->create_partial_order();
4623 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4624 - } catch (Exception $e) {
4625 - $this->log($e->getMessage(),'error');
4626 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4627 - wp_send_json($result);
4628 - exit();
4629 - }
4630 - if (!$orderid) {
4631 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4632 - wp_send_json($result);
4633 - exit();
4849 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4850 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4851 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4852 + $cookies = $args['cookies'] ?? [];
4853 + foreach($cookies as $key => $value) {
4854 + if (!isset($_COOKIE[$key])) {
4855 + $_COOKIE[$key] = $value;
4856 + }
4634 4857 }
4858 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4859 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4860 + $others =$args['post'] ?? [];
4861 + foreach($args['post'] as $key=>$value) {
4862 + $_POST[$key] = $value;
4863 + }
4635 4864
4636 - try {
4637 - $this->maybe_add_static_shipping($gw,$orderid);
4638 - } catch (Exception $e) {
4639 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4640 - $this->log($e->getMessage(),'error');
4641 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4642 - wp_send_json($result);
4643 - exit();
4865 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4866 + $current_hash = $this->create_order_hash();
4867 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4868 + if (!$confirmation) {
4869 + $result = $this->express_order_needs_confirmation($args, null, $current_hash);
4870 + if (!empty($result)) {
4871 + return $result;
4872 + }
4644 4873 }
4645 -
4646 - $ok = $gw->process_payment($orderid);
4647 - if ($ok && $ok['result'] == 'success') {
4648 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4649 - wp_send_json($result);
4650 - exit();
4874 +
4875 + $result = $this->create_and_process_express_order();
4876 + if ($result['ok'] == 1) {
4877 + $orderid = $result['orderid'];
4878 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4879 + WC()->session->save_data();
4651 4880 }
4652 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4653 - wp_send_json($result);
4654 - exit();
4881 + return $result;
4882 +
4655 4883 }
4656 4884
4657 - // Same as ajax_do_express_checkout, but for a single product/variation. Duplicate code because we want to manipulate the cart differently here. IOK 2018-09-25
4658 - public function ajax_do_single_product_express_checkout() {
4659 - check_ajax_referer('do_express','sec');
4660 - Vipps::nocache();
4885 +
4886 + // Rest handler for single product express checkout. Expects arguments as JSON. IOK 2026-08-25
4887 + public function rest_do_single_product_express_checkout ($request) {
4888 + Vipps::nocache();
4661 4889 static::set_locale_if_in_header();
4662 - require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4663 - $gw = $this->gateway();
4664 -
4665 - if (!$gw->express_checkout_available()) {
4666 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4667 - wp_send_json($result);
4668 - exit();
4890 + $args = $request->get_json_params();
4891 + if (!$args) {
4892 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4669 4893 }
4894 + $result = ['ok' => 0, 'msg'=>'', 'orderid'=>0, 'url'=>''];
4670 4895
4896 + // We receive the varid, prodid, sku and quantity directly. One of these. The sku is the dominant one. IOK 2026-08-27
4897 + $varid = intval($args['variation_id'] ?? 0);
4898 + $prodid = intval($args['product_id'] ?? 0);
4899 + $sku = sanitize_text_field($args['sku'] ?? "");
4900 + $quantity = max(1, intval($args['quantity'] ?? 0));
4671 4901
4672 - // Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but
4673 - // we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02
4674 - $varid = intval(@$_POST['variation_id']);
4675 - $prodid = intval(@$_POST['product_id']);
4676 - $sku = sanitize_text_field(@$_POST['sku']);
4677 - $quant = intval(@$_POST['quantity']);
4678 4902
4679 - // Get any attributes posted for variable products (where one of the dimensions is "any" for instance)
4680 - $variations = array();
4681 - foreach ($_POST as $key => $value ) {
4903 + // We expect the variations - that is, the fields named "attribute_..." to be sent as post fields.
4904 + // We just need to sanitize them.
4905 + $variations = [];
4906 + $invars = $args['post'] ?? [];
4907 + foreach ($invars as $key => $value) {
4682 4908 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4683 4909 continue;
4684 4910 }
4685 4911 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
@@ -4684,15 +4910,94 @@
4684 4910 }
4685 4911 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4686 4912 }
4687 4913
4688 - $product = null;
4689 - $variant = null;
4690 - $parent = null;
4691 - $parentid = null;
4692 - $quantity = 1;
4693 - if ($quant && $quant>1) $quantity=$quant;
4914 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4915 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4916 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4917 + $cookies = $args['cookies'] ?? [];
4918 + foreach($cookies as $key => $value) {
4919 + if (!isset($_COOKIE[$key])) {
4920 + $_COOKIE[$key] = $value;
4921 + }
4922 + }
4694 4923
4924 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4925 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4926 + $others =$args['post'] ?? [];
4927 + foreach($args['post'] as $key=>$value) {
4928 + $_POST[$key] = $value;
4929 + }
4930 +
4931 + // Since this is the REST api, we need to load the cart manually here. *Not* loading the cart could be an option but unpredictable. IOK 2026-08-27
4932 + if ( is_null( WC()->cart ) ) {
4933 + WC()->frontend_includes();
4934 + if ( ! WC()->session instanceof WC_Session ) {
4935 + WC()->session = new WC_Session_Handler();
4936 + WC()->session->init();
4937 +
4938 + // If we don't have a session cookie, we need to set it, and also initialize the $_COOKIE value. IOK 2026-09-29
4939 + if (! WC()->session->get_session_cookie()) {
4940 + $store_session_cookie = function ( $options, $name, $value ) { $_COOKIE[$name] = $value; return $options;};
4941 + add_filter('woocommerce_set_cookie_options', $store_session_cookie, 10, 3);
4942 + try {
4943 + WC()->session->set_customer_session_cookie( true ); // We have to explicitly set the cookie if this session is fresh. IOK 2026-09-29
4944 + } finally {
4945 + remove_filter('woocommerce_set_cookie_options', $store_session_cookie, 10);
4946 + }
4947 + }
4948 + }
4949 + if (is_null( WC()->customer)) {
4950 + WC()->customer = new WC_Customer( get_current_user_id(), true );
4951 + }
4952 + WC()->cart = new WC_Cart();
4953 + WC()->cart->get_cart_from_session();
4954 + }
4955 +
4956 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4957 + // We calculate this here so we can add it to the session later. IOK 2026-09-09
4958 + $orderspec = array('sku'=> $sku, 'product_id'=>$prodid, 'variation_id'=>$varid, 'quantity'=>$quantity);
4959 + $current_hash = $this->create_order_hash($orderspec);
4960 +
4961 + // Now to handle "extra questions" for an order, including terms + conditions and "possible duplicate order" IOK 2026-09-09
4962 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4963 + if (!$confirmation) {
4964 + $result = $this->express_order_needs_confirmation($args, $orderspec, $current_hash);
4965 + if (!empty($result)) {
4966 + $response = new WP_REST_Response($result);
4967 + $response->set_status(200);
4968 + return $response;
4969 + }
4970 + }
4971 +
4972 + // Basically always return 200 after this, and always return an object with an 'ok' and a 'msg' value, possibly 'orderid' and 'url'.
4973 + $result = $this->really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity, $variations);
4974 + // And if we're going to express now so let's note the order. IOK 2026-08-27. Now this assumes success, but *basically* I think this is ok.
4975 + // We'll reset it on order failure I think. IOK 2026-08-20 FIXME
4976 + if ($result['ok'] == 1) {
4977 + $orderid = $result['orderid'];
4978 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4979 + WC()->session->save_data();
4980 + }
4981 +
4982 + $response = new WP_REST_Response($result);
4983 + $response->set_status(200);
4984 +
4985 + return $response;
4986 + }
4987 +
4988 + // Common private method to do single product express checkout, used by the new REST express. IOK 2026-08-25
4989 + private function really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity=1, $variations=[]) {
4990 + require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4991 + $gw = $this->gateway();
4992 +
4993 + if (!$gw->express_checkout_available()) {
4994 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4995 + return $result;
4996 + }
4997 + // Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but
4998 + // we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02
4999 +
4695 5000 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
4696 5001 // Moved rules around as the _sku variant broke in 3.6.1 for stores that didn't bother to update the database IOK 2019-04-24
4697 5002 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
4698 5003 try {
@@ -4705,17 +5010,14 @@
4705 5010 $product = wc_get_product($skuid);
4706 5011 }
4707 5012 } catch (Exception $e) {
4708 5013 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
4709 - wp_send_json($result);
4710 - exit();
5014 + return $result;
4711 5015 }
4712 5016
4713 -
4714 5017 if (!$product) {
4715 5018 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
4716 - wp_send_json($result);
4717 - exit();
5019 + return $result;
4718 5020 }
4719 5021
4720 5022 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
4721 5023 $parent = $parentid ? wc_get_product($parentid) : null;
@@ -4722,34 +5024,30 @@
4722 5024
4723 5025 // This can't really happen, but if it did..
4724 5026 if ($prodid && $parentid && ($prodid != $parentid)) {
4725 5027 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
4726 - wp_send_json($result);
4727 - exit();
5028 + return $result;
4728 5029 }
4729 5030 if (!$gw->product_supports_express_checkout($product)) {
4730 5031 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4731 - wp_send_json($result);
4732 - exit();
5032 + return $result;
4733 5033 }
4734 5034
4735 5035 // Somebody addded the wrong SKU
4736 5036 if ($product->get_type() == 'variable'){
4737 5037 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
4738 - wp_send_json($result);
4739 - exit();
5038 + return $result;
4740 5039 }
4741 5040 // Final check of availability
4742 5041 if (!$product->is_purchasable() || !$product->is_in_stock()) {
4743 5042 $result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false);
4744 - wp_send_json($result);
4745 - exit();
5043 + return $result;
4746 5044 }
4747 5045
4748 5046 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
4749 -
4750 5047 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
4751 5048 // because some plugins actually override this.
5049 + // NB: Please note the cart must have been loaded here, be aware when doing REST. IOK 2026-08-27
4752 5050 $current_cart = clone WC()->cart;
4753 5051 WC()->cart->empty_cart();
4754 5052
4755 5053 if ($parent && $parent->get_type() == 'variable') {
@@ -4756,50 +5054,22 @@
4756 5054 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
4757 5055 } else {
4758 5056 WC()->cart->add_to_cart($product->get_id(),$quantity);
4759 5057 }
5058 + WC()->session->save_data();
4760 5059
4761 - try {
4762 - $orderid = $gw->create_partial_order();
4763 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4764 - } catch (Exception $e) {
4765 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4766 - wp_send_json($result);
4767 - exit();
4768 - }
5060 + $result = $this->create_and_process_express_order();
4769 5061
4770 - if (!$orderid) {
4771 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4772 - wp_send_json($result);
4773 - exit();
5062 + if ($result['ok'] ?? false) {
5063 + // Single product purchase, so save any contents of the real cart
5064 + $orderid = $result['orderid'];
5065 + $order = wc_get_order($orderid);
5066 + $order->update_meta_data('_vipps_single_product_express',true);
5067 + $order->save();
5068 + $this->save_cart($order,$current_cart);
4774 5069 }
4775 5070
4776 - try {
4777 - $this->maybe_add_static_shipping($gw,$orderid);
4778 - } catch (Exception $e) {
4779 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4780 - $this->log($e->getMessage(),'error');
4781 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4782 - wp_send_json($result);
4783 - exit();
4784 - }
4785 -
4786 -
4787 - // Single product purchase, so save any contents of the real cart
4788 - $order = wc_get_order($orderid);
4789 - $order->update_meta_data('_vipps_single_product_express',true);
4790 - $order->save();
4791 - $this->save_cart($order,$current_cart);
4792 -
4793 - $ok = $gw->process_payment($orderid);
4794 - if ($ok && $ok['result'] == 'success') {
4795 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4796 - wp_send_json($result);
4797 - exit();
4798 - }
4799 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4800 - wp_send_json($result);
4801 - exit();
5071 + return $result;
4802 5072 }
4803 5073
4804 5074 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
4805 5075 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
@@ -4842,9 +5112,9 @@
4842 5112 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4843 5113 }
4844 5114 }
4845 5115
4846 - // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
5116 + // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4847 5117 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4848 5118 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4849 5119 // If we need to add more shipping methods *before* the shipping callback starts, it must be done before we load the session. IOK 2025-05-06
4850 5120 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
@@ -4865,9 +5135,9 @@
4865 5135 $transaction = sanitize_text_field(@$_POST['transaction']);
4866 5136
4867 5137 $sessionorders= WC()->session->get('_vipps_session_orders');
4868 5138 if (!isset($sessionorders[$orderid])) {
4869 - wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps')));
5139 + wp_send_json(array('status'=>'error', 'msg'=>__('Not a session order','woo-vipps')));
4870 5140 }
4871 5141
4872 5142 $order = wc_get_order($orderid);
4873 5143 if (!$order) {
@@ -4915,46 +5185,37 @@
4915 5185 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4916 5186 return false;
4917 5187 }
4918 5188
4919 - // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4920 - // These are for the "fallback URL" mostly. IOK 2018-05-18
4921 - private function make_vipps_url($what) {
4922 - if ( !get_option('permalink_structure')) {
4923 - return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4924 - }
4925 - return trailingslashit(home_url($what, 'https'));
5189 + // The various return URLs for special pages of the Vipps stuff. Previously used a fake page and had to check permalink_structure. LP 2026-08-26
5190 + private function make_special_page_url($action) {
5191 + return add_query_arg('action', $action, $this->get_special_page_url());
4926 5192 }
5193 +
4927 5194 public function payment_return_url() {
4928 - return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
5195 + return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
4929 5196 }
4930 5197 public function express_checkout_url() {
4931 - return $this->make_vipps_url('vipps-express-checkout');
5198 + return $this->make_special_page_url('do_express_checkout');
4932 5199 }
4933 5200 public function buy_product_url() {
4934 - return $this->make_vipps_url('vipps-buy-product');
5201 + return $this->make_special_page_url('buy_product');
4935 5202 }
4936 5203
4937 - // Return the method in the Vipps
4938 - public function is_special_page() {
4939 - $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4940 - $method = null;
4941 - if ( get_option('permalink_structure')) {
4942 - foreach($specials as $special=>$specialmethod) {
4943 - // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4944 - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4945 - if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4946 - $method = $specialmethod; break;
4947 - }
4948 - }
4949 - } else {
4950 - if (isset($_GET['VippsSpecialPage'])) {
4951 - $method = @$specials[$_GET['VippsSpecialPage']];
4952 - }
4953 - }
4954 - return $method;
5204 + public static function is_special_page() {
5205 + $id = static::get_special_page_id();
5206 + return $id && is_page($id);
4955 5207 }
4956 5208
5209 + public static function get_special_page_id() {
5210 + $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5211 + return $id > 0 ? $id : null;
5212 + }
5213 +
5214 + public static function get_special_page_url() {
5215 + return get_permalink(static::get_special_page_id());
5216 + }
5217 +
4957 5218 // Just create a spinner and a overlay.
4958 5219 public function spinner () {
4959 5220 $flavour = sanitize_title($this->get_payment_method_name());
4960 5221 ob_start();
@@ -5001,16 +5262,8 @@
5001 5262 }
5002 5263 return null;
5003 5264 }
5004 5265
5005 - // DEPRECATED: Legacy function as of new web component express buttons. see get_buy_now_button and get_html_button. LP 2026-06-26
5006 - public function get_buy_now_button_manual($product_id, $variation_id=null, $sku=null, $disabled=false, $classes='',
5007 - $_logo_variant=null, $_logo_lang=null, // deprecated params
5008 - $context='global', $button_args_override = [],
5009 - ) {
5010 - return $this->get_buy_now_button($product_id, $variation_id, $sku, $disabled, $classes, $context, $button_args_override);
5011 - }
5012 -
5013 5266 // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5014 5267 // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5015 5268 // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5016 5269 public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
@@ -5029,9 +5282,8 @@
5029 5282 if ($sku) $data['product_sku'] = $sku;
5030 5283 if ($product_id) $data['product_id'] = $product_id;
5031 5284 if ($variation_id) $data['variation_id'] = $variation_id;
5032 5285
5033 -
5034 5286 $buttoncode = "<a href='javascript:void(0)' $disabled ";
5035 5287 foreach($data as $key=>$value) {
5036 5288 $value = esc_attr($value);
5037 5289 $buttoncode .= " data-$key='$value' ";
@@ -5055,8 +5307,11 @@
5055 5307 if ($classes) $classes = " $classes";
5056 5308 if ($short) $classes = "short $classes";
5057 5309
5058 5310 $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5311 +
5312 +
5313 +
5059 5314 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
5060 5315 }
5061 5316
5062 5317 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
@@ -5139,51 +5394,90 @@
5139 5394 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5140 5395 }
5141 5396
5142 5397
5143 -
5144 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5398 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5145 5399 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5400 + // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5146 5401 public function woocommerce_create_pages ($data) {
5402 + // Vipps Checkout page
5147 5403 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5148 - if (!$vipps_checkout_activated) return $data;
5404 + if ($vipps_checkout_activated) {
5405 + $data['vipps_checkout'] = array(
5406 + 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5407 + 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5408 + 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5409 + );
5410 + }
5149 5411
5150 - $data['vipps_checkout'] = array(
5151 - 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5152 - 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5153 - 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5154 - );
5155 -
5412 + // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5413 + $data['vipps_special_page'] = [
5414 + 'name' => 'vipps-payment', // slug
5415 + /* translators: company name */
5416 + 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5417 + 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5418 + ];
5156 5419 return $data;
5157 5420 }
5158 5421
5159 - // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5422 + // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5423 + // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5160 5424 public function maybe_create_vipps_pages () {
5425 + $make_pages = false;
5426 +
5427 + // Vipps Checkout page. LP 2026-08-18
5161 5428 $checkoutid = wc_get_page_id('vipps_checkout');
5162 - $makeit = !$checkoutid || ! get_post_status($checkoutid);
5163 - if ($makeit) {
5429 + if (!$checkoutid || ! get_post_status($checkoutid)) {
5164 5430 delete_option('woocommerce_vipps_checkout_page_id');
5431 + $make_pages = true;
5165 5432 }
5166 5433
5167 - if ($makeit) {
5168 - WC_Install::create_pages();
5434 + // vipps special page, previously a fake page. LP 2026-08-18
5435 + $builtin_special_page_id = static::get_special_page_id();
5436 + if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5437 + delete_option('woocommerce_vipps_special_page_page_id');
5438 + $make_pages = true;
5169 5439 }
5440 +
5441 + if ($make_pages) {
5442 + WC_Install::create_pages();
5443 + }
5170 5444 }
5171 5445
5446 + public function vipps_special_page_shortcode($atts, $content) {
5447 + // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5448 + if (is_admin()) return;
5449 + if (wp_doing_ajax()) return;
5450 + if (defined('REST_REQUEST') && REST_REQUEST) return;
5451 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5172 5452
5453 + $action = $_GET['action'] ?? '';
5454 + $html = "";
5455 + switch ($action) {
5456 + case 'wait_for_payment':
5457 + $html = $this->vipps_wait_for_payment();
5458 + break;
5459 + case 'do_express_checkout':
5460 + $html = $this->vipps_express_checkout();
5461 + break;
5462 + case 'buy_product':
5463 + $html = $this->vipps_buy_product();
5464 + break;
5465 + default:
5466 + $html = '';
5467 + }
5468 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5469 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5470 +
5471 + // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5472 + return $html;
5473 + }
5474 +
5475 +
5173 5476 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5174 5477 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5175 5478 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5176 5479 public function vipps_buy_product() {
5177 - status_header(200,'OK');
5178 - Vipps::nocache();
5179 -
5180 - add_filter('body_class', function ($classes) {
5181 - $classes[] = 'vipps-express-checkout';
5182 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5183 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5184 - });
5185 -
5186 5480 do_action('woo_vipps_express_checkout_page');
5187 5481
5188 5482 $session = WC()->session;
5189 5483 $posted = $session->get('__vipps_buy_product');
@@ -5212,39 +5506,38 @@
5212 5506
5213 5507 if (!$productinfo) {
5214 5508 $title = __("Product is no longer available",'woo-vipps');
5215 5509 $content = __("The link you have followed is for a product that is no longer available at this location. Please return to the store and try again",'woo-vipps');
5216 - return $this->fakepage($title,$content);
5510 + return $this->special_page_html($title,$content);
5217 5511 }
5218 5512
5219 5513 // Pass the productinfo to the express checkout form
5220 5514 $args = array();
5221 - $args['quantity'] = 1;
5222 - if (array_key_exists('product_id',$productinfo)) $args['product_id'] = intval($productinfo['product_id']);
5223 - if (array_key_exists('variation_id',$productinfo)) $args['variation_id'] = intval($productinfo['variation_id']);
5224 - if (array_key_exists('product_sku',$productinfo)) $args['sku'] = sanitize_text_field($productinfo['product_sku']);
5225 - if (array_key_exists('quantity',$productinfo)) $args['quantity'] = intval($productinfo['quantity']);
5515 + $args['product_id'] = esc_attr(intval($productinfo['product_id'] ?? 0));
5516 + $args['variation_id'] = esc_attr(intval($productinfo['variation_id'] ?? 0));
5517 + $args['sku'] = esc_attr(sanitize_text_field($productinfo['product_sku'] ?? ""));
5518 + $args['quantity'] = esc_attr(max(1, intval($productinfo['quantity'] ?? 0)));
5226 5519
5227 - // For variable products where some of the attributes are "any", we need to add these as well. This is from woos form-handler for these.
5228 - foreach ($productinfo as $key => $value) {
5229 - if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
5230 - continue;
5231 - }
5232 - $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5233 - }
5520 + $payment_method = $this->get_payment_method_name();
5521 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5522 + $bclass = esc_attr($payment_method);
5234 5523
5235 - $this->print_express_checkout_page(true,'do_single_product_express_checkout',$args);
5524 + $content = "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5525 + $content .= "<div class='vipps-qr-purchase' style='visibility:hidden'>";
5526 + $content .= "<a href='javascript:void(0)' class='single-product button vipps-buy-now $bclass' data-vipps-autostart='true' data-vipps-purchase='single' data-product_id='{$args['product_id']}' data-variation_id='{$args['variation_id']}' data-product_sku='{$args['sku']}' data-quantity='{$args['quantity']}' title='{$btitle}';
5527 + >";
5528 + $content .= $this->get_html_button_for_context('global');
5529 + $content .= "</a>";
5530 + $content .= "</div>";
5531 +
5532 + return $content;
5236 5533 }
5237 5534
5238 - // This is a landing page for the express checkout of then normal cart - it is done like this because this could take time on slower hosts.
5239 - public function vipps_express_checkout() {
5240 - status_header(200,'OK');
5241 - Vipps::nocache();
5535 + public function vipps_express_checkout_consistency_check() {
5242 5536 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5243 5537 // IOK 2018-05-28
5244 5538 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5245 5539
5246 -
5247 5540 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5248 5541 if (!$backurl) $backurl = home_url();
5249 5542
5250 5543 if (!$ok) {
@@ -5258,219 +5551,39 @@
5258 5551 wp_redirect($backurl);
5259 5552 exit();
5260 5553 }
5261 5554
5262 - add_filter('body_class', function ($classes) {
5263 - $classes[] = 'vipps-express-checkout';
5264 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5265 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5266 - });
5267 -
5268 - do_action('woo_vipps_express_checkout_page');
5269 -
5270 - $this->print_express_checkout_page(true, 'do_express_checkout');
5555 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5271 5556 }
5272 5557
5273 - // This method tries to ensure that a customer does not 'lose' the return page and
5274 - // starts ordering the same products twice. IOK 2020-01-22
5275 - protected function validate_express_checkout_orderspec ($orderspec) {
5276 - if (empty($orderspec)) return true; // It's not a duplicate, it's nothing.
5277 -
5278 - // First build for the current order an array of hash-tables keyed by prodid, varid and quantity.
5279 - $orderset = array();
5280 - foreach($orderspec as $entry) $orderset[] = join(':', $entry);
5281 -
5282 - // Then get open orders
5283 - $sessionorders = array();
5284 - $sessionorderdata = WC()->session->get('_vipps_session_orders');
5285 - if ($sessionorderdata) {
5286 - foreach(array_keys($sessionorderdata) as $oid) {
5287 - $orderobject = wc_get_order($oid);
5288 - // Check to see that this hasn't been deleted yet IOK 2020-01-07
5289 - if ($orderobject instanceof WC_Order) {
5290 - $sessionorders[] = $orderobject;
5291 - }
5292 - }
5558 + // This is a landing page for the express checkout of the normal cart - it is done like this because this could take time on slower hosts.
5559 + // IOK 2026-09-09 - nowadays this is only used for compatibility mode. It will automatically start express checkout of the current cart when reached.
5560 + public function vipps_express_checkout() {
5561 + // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5562 + if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5563 + $content = __('Link expired, please try again', 'woo-vipps');
5564 + return $content;
5293 5565 }
5294 - // Nothing more to do here
5295 - if (empty($sessionorders)) return true;
5566 +
5567 + do_action('woo_vipps_express_checkout_page');
5296 5568
5297 - // And create a similar hash table for each of the open orders
5298 - $openorderdata = array();
5299 - foreach ($sessionorders as $open_order) {
5300 - $status = $open_order->get_status();
5301 - if ($status == 'cancelled' || $status == 'pending') continue;
5302 - $when = strtotime($open_order->get_date_modified());
5303 - $cutoff = $when + apply_filters('woo_vipps_recent_order_cutoff', (5*60));
5304 - if (time() > $cutoff) {
5305 - continue;
5306 - }
5307 - $orderdata = array();
5308 - foreach($open_order->get_items() as $item) {
5309 - $productspec = $item->get_product_id() . ':' . $item->get_variation_id() . ':' . $item->get_quantity();
5310 - $orderdata[] = $productspec;
5311 - }
5312 - $openorderdata[]=$orderdata;
5313 - }
5569 + $payment_method = $this->get_payment_method_name();
5570 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5571 + $bclass = esc_attr($payment_method);
5572 + $sec = esc_attr($_REQUEST['sec']);
5573 + $content = "";
5574 + $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5575 + $content .= '<div class="vipps-cart-purchase" style="visibility:hidden">"';
5576 + $content .= "<a href='javascript:void(0)' class='vipps-express-checkout short $bclass' data-vipps-autostart='true' data-sec='$sec' title='$btitle'>";
5577 + $content .= $this->get_html_button_for_context('global');
5578 + $content .="</a>";
5579 + $content .="</div>";
5314 5580
5315 - // Now: For each entry in the orderhash, check if there is an order that has a) all of them and b) not any more of them.
5316 - foreach($openorderdata as $prevorder) {
5317 - $a = array_diff($prevorder, $orderset);
5318 - $b = array_diff($orderset, $prevorder);
5319 - if (empty($a) && empty($b)) {
5320 - $this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info');
5321 - return false;
5322 - }
5323 - }
5324 - // Else, order is good.
5325 - return true;
5581 + return $content;
5326 5582 }
5327 5583
5328 - // Returns a triple of productid, variantid and quantity from an array of arguments which can pass either these or a SKU value.
5329 - // Return value is like in a cart.
5330 - // Used to create an order in express checkout, and to see that this order isn't a repeat. IOK 2020-01-22
5331 - protected function get_orderspec_from_arguments ($productinfo) {
5332 - if (!$productinfo) return array();
5333 - $variantid = 0;
5334 - $productid = 0;
5335 - $quantity = intval(@$productinfo['quantity']);
5336 - if (!$quantity) $quantity = 1;
5337 - if (isset($productinfo['sku']) && $productinfo['sku']) {
5338 - $sku = $productinfo['sku'];
5339 - $skuid = wc_get_product_id_by_sku($sku);
5340 - $product = wc_get_product($skuid);
5341 - $parentid = $product ? $product->get_parent_id() : null;
5342 - if ($product) {
5343 - if ($parentid) {
5344 - $variantid = $skuid; $productid = $parentid;
5345 - } else {
5346 - $productid = $skuid;
5347 - }
5348 - }
5349 - } else if (isset($productinfo['product_id']) && $productinfo['product_id']) {
5350 - $productid = intval($productinfo['product_id']);
5351 - $variantid = intval(@$productinfo['variation_id']);
5352 - }
5353 - if ($productid) return array(array('product_id'=>$productid, 'variation_id'=>$variantid, 'quantity'=>$quantity));
5354 - return array();
5355 - }
5356 - // If no productinfo, this will produce an orderspec from the current cart IOK 2020-01-24
5357 - protected function get_orderspec_from_cart () {
5358 - $cartitems = WC()->cart->get_cart();
5359 - $orderspec = array();
5360 - foreach($cartitems as $item => $values) {
5361 - $orderspec[] = array('product_id'=>$values['product_id'], 'variation_id'=>$values['variation_id'], 'quantity'=>$values['quantity']);
5362 - }
5363 - return $orderspec;
5364 - }
5365 -
5366 - // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5367 - protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5368 - $gw = $this->gateway();
5369 -
5370 - $expressCheckoutMessages = array();
5371 - $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
5372 - $expressCheckoutMessages['temporaryError'] = sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name());
5373 - $expressCheckoutMessages['successMessage'] = sprintf(__('To the %1$s app!','woo-vipps'), $this->get_payment_method_name());
5374 -
5375 - wp_register_script('vipps-express-checkout',plugins_url('js/express-checkout.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/express-checkout.js"), 'true');
5376 - wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5377 - wp_enqueue_script('vipps-express-checkout');
5378 - // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5379 - // to actually perform the express checkout.
5380 - $buttonhtml = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button());
5381 -
5382 -
5383 -
5384 - $orderspec = $this->get_orderspec_from_arguments($productinfo);
5385 - if (empty($orderspec)) {
5386 - $orderspec = $this->get_orderspec_from_cart();
5387 - }
5388 - $orderisOK = $this->validate_express_checkout_orderspec($orderspec);
5389 - $orderisOK = apply_filters('woo_vipps_validate_express_checkout_orderspec', $orderisOK, $orderspec);
5390 -
5391 - $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
5392 - $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
5393 - $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
5394 -
5395 - $askForConfirmationHTML = '';
5396 - if (!$orderisOK) {
5397 - $header = __("Are you sure?",'woo-vipps');
5398 - $body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps');
5399 - $askForConfirmationHTML = apply_filters('woo_vipps_ask_user_to_confirm_repurchase', "<h2 class='confirmVippsExpressCheckoutHeader'>$header</h2><p>$body</p>");
5400 - }
5401 - // Should we go directly to checkout, or do we need to stop and ask the user something (for instance?) IOK 2010-01-20
5402 - $execute = $execute && $orderisOK && !$askForTerms;
5403 - $execute = apply_filters('woo_vipps_checkout_directly_to_vipps', $execute, $productinfo);
5404 -
5405 - $content = $this->spinner();
5406 -
5407 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5408 - // The form data below is sent on order creation; the sec is also used to poll session status
5409 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5410 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5411 - $content .= "<input type='hidden' name='action' value='" . esc_attr($action) ."'>";
5412 - if ($this->gateway()->get_option('vippsorderattribution') == 'yes') {
5413 - // This is for the new order attribution feature of woo. IOK 2024-01-09
5414 - $content .= '<input type="hidden" id="vippsorderattribution" value="1" />';
5415 - ob_start();
5416 - do_action( 'woocommerce_after_order_notes');
5417 - $content .= ob_get_clean();
5418 - }
5419 - $content .= wp_nonce_field('do_express','sec',1,false);
5420 -
5421 - $termsHTML = '';
5422 - if ($askForTerms) {
5423 - // Include shop terms
5424 - ob_start();
5425 - wc_get_template('checkout/terms.php');
5426 - $termsHTML = ob_get_clean();
5427 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5428 - }
5429 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5430 -
5431 - if ($productinfo) {
5432 - foreach($productinfo as $key=>$value) {
5433 - $k = esc_attr($key);
5434 - $v = esc_attr($value);
5435 - $content .= "<input type='hidden' name='$k' value='$v' />";
5436 - }
5437 - }
5438 - ob_start();
5439 - $content .= do_action('woo_vipps_express_checkout_orderspec_form', $productinfo);
5440 - $content .= ob_get_clean();
5441 - $content .= "</form>";
5442 -
5443 - $extraHTML = apply_filters('woo_vipps_express_checkout_final_html', '', $termsHTML,$askForConfirmationHTML);
5444 - $pressTheButtonHTML = "";
5445 - if (empty($termsHTML) && empty($askForConfirmationHTML) && empty($extraHTML)) {
5446 - $pressTheButtonHTML = "<p id=waiting>" . sprintf(__('Ready for %1$s - press the button', 'woo-vipps'), Vipps::ExpressCheckoutName()) . "</p>";
5447 - }
5448 -
5449 - if ($execute) {
5450 - $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5451 - $content .= "<div id='vipps-status-message'></div>";
5452 - $this->fakepage(__('Order in progress','woo-vipps'), $content);
5453 - return;
5454 - } else {
5455 - $content .= $askForConfirmationHTML;
5456 - $content .= $extraHTML;
5457 - $content .= $termsHTML;
5458 - $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5459 - $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5460 - $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>";
5461 - $content .= "<div id='vipps-status-message'></div>";
5462 - $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5463 - return;
5464 - }
5465 - }
5466 -
5467 -
5468 -
5469 - public function vipps_wait_for_payment() {
5470 - status_header(200,'OK');
5471 - Vipps::nocache();
5472 -
5584 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5585 + private function handle_payment_poll_and_redirect () {
5473 5586 $orderid = WC()->session->get('_vipps_pending_order');
5474 5587
5475 5588 $order = null;
5476 5589 $gw = $this->gateway();
@@ -5484,9 +5597,9 @@
5484 5597 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5485 5598 // simulating the session with that.
5486 5599 // IOK 2019-11-19, changed to using GET 2023-01-23
5487 5600 if ($no_session && $limited_session) {
5488 - $orderid = intval(@$_GET['id']);
5601 + $orderid = intval($_GET['id'] ?? false);
5489 5602 }
5490 5603 if ($orderid) {
5491 5604 clean_post_cache($orderid);
5492 5605 $order = wc_get_order($orderid);
@@ -5501,20 +5614,22 @@
5501 5614 $session = WC()->session;
5502 5615 if (!$session->has_session()) {
5503 5616 $session->set_customer_session_cookie(true);
5504 5617 }
5618 +
5619 + $sessionorders= WC()->session->get('_vipps_session_orders');
5620 + $sessionorders[$orderid] = 1;
5621 + WC()->session->set('_vipps_session_orders',$sessionorders);
5505 5622 $session->set('_vipps_pending_order', $orderid);
5623 + WC()->session->save_data();
5506 5624 }
5507 5625 }
5508 5626
5509 -
5510 - do_action('woo_vipps_wait_for_payment_page',$order);
5511 -
5512 5627 $deleted_order=0;
5513 5628 if ($orderid && !$order) {
5514 5629 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5515 5630 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
5516 - $this->log(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), 'debug');
5631 + $this->log(sprintf(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), $orderid), 'debug');
5517 5632 $deleted_order=1;
5518 5633 }
5519 5634
5520 5635 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
@@ -5524,12 +5639,13 @@
5524 5639
5525 5640 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5526 5641 $do_poll = true;
5527 5642
5528 - // Still pending, no callback. Make a call to the server as the order might not have been created. IOK 2018-05-16
5643 + // Do a single poll here to check and set the order status at Woo using the order status at Vipps IOK 2026-09-29
5529 5644 if ($do_poll && $status == 'pending') {
5530 - // Just in case the callback hasn't come yet, do a quick check of the order status at Vipps.
5531 - $newstatus = $gw->callback_check_order_status($order);
5645 + // We will do *one* poll before waiting for the callback (for a while, at least.) IOK 2026-09-29
5646 + $newstatus = $gw->poll_and_check_order_status($order);
5647 + $this->log(sprintf(__("In order return: Order status of %1\$d is %2\$s", 'woo-vipps'), $orderid, $newstatus), 'info');
5532 5648 if ($status != $newstatus) {
5533 5649 $status = $newstatus;
5534 5650 clean_post_cache($orderid);
5535 5651 $order = wc_get_order($orderid); // Reload order object
@@ -5534,11 +5650,13 @@
5534 5650 clean_post_cache($orderid);
5535 5651 $order = wc_get_order($orderid); // Reload order object
5536 5652 }
5537 5653 } else {
5538 - // No need to do anyting here. IOK 2020-01-26
5654 + // No need to do anyting here. IOK 2020-01-26
5539 5655 }
5540 5656
5657 + // Actually, this may cause a second poll if the first left us pending. Should be rewritten - but *mostly* it will just check
5658 + // the payment status at Vipps without polls, which will tell us if the payment succeeded in case people use custom order statuses and so on. IOK 2026-09-29
5541 5659 $payment = 'notchecked';
5542 5660 if ($do_poll) {
5543 5661 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5544 5662 }
@@ -5552,9 +5670,8 @@
5552 5670 exit();
5553 5671 }
5554 5672
5555 5673 // We are done, but in failure. Don't poll.
5556 - $content = "";
5557 5674 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5558 5675
5559 5676 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5560 5677 // For failed it shows a "Retry payment" button that takes the customer to /pay-for-order where it will be retried. LP 2026-03-17
@@ -5562,8 +5679,9 @@
5562 5679 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5563 5680 wp_redirect($failure_redirect);
5564 5681 exit();
5565 5682 }
5683 +
5566 5684 if ($status == 'cancelled' || $payment == 'cancelled') {
5567 5685 $this->maybe_restore_cart($orderid,'failed');
5568 5686 if ($failure_redirect){
5569 5687 wp_redirect($failure_redirect);
@@ -5568,27 +5686,46 @@
5568 5686 if ($failure_redirect){
5569 5687 wp_redirect($failure_redirect);
5570 5688 exit();
5571 5689 }
5690 + } else {
5691 + // If not, enqueue the status checker IOK 2026-09-21
5692 + wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5693 + }
5694 +
5695 + $this->log(sprintf(__("Order status of %1\$d not ready in order return: payment status %2\$s", 'woo-vipps'), $orderid, $payment), 'info');
5696 +
5697 + // Communicate this to the shortcode IOK 2026-09-21
5698 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5699 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5700 + });
5701 +
5702 + }
5703 +
5704 + public function vipps_wait_for_payment() {
5705 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5706 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5707 +
5708 + $orderid = $data['orderid'] ?? 0;
5709 + $status = $data['status'] ?? "";
5710 + $payment = $data['payment'] ?? "";
5711 +
5712 + $order = wc_get_order($orderid);
5713 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5714 +
5715 + do_action('woo_vipps_wait_for_payment_page',$order);
5716 + $gw = $this->gateway();
5717 +
5718 + $content = "";
5719 + if ($status == 'cancelled' || $payment == 'cancelled') {
5572 5720 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5573 5721 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5574 5722 $content .= "</div>";
5575 - $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5576 -
5577 - return;
5723 + return $this->special_page_html('', $content);
5578 5724 }
5579 5725
5580 5726 // Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27
5581 -
5582 5727 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5583 - wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5584 -
5585 - // Check that order exists and belongs to our session. Can use WC()->session->get() I guess - set the orderid or a hash value in the session
5586 - // and check that the order matches (and is 'pending') (and exists)
5587 - $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5588 - $vippsstatus = $order->get_meta('_vipps_status');
5589 - $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5590 -
5591 5728 $signal = $this->callbackSignal($order);
5592 5729 $content = "";
5593 5730 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5594 5731
@@ -5596,18 +5733,18 @@
5596 5733 $signalurl = $this->callbackSignalURL($signal);
5597 5734
5598 5735 $content .= "</p></div>";
5599 5736
5600 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5601 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5602 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5737 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5738 +
5739 + // Carry the order status to the checking script IOK 2026-09-21
5740 + $content .= "<form id='vippsdata'>";
5603 5741 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5604 5742 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5605 5743 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5606 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5744 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5607 5745 $content .= "</form>";
5608 5746
5609 -
5610 5747 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5611 5748 $content .= "<p>" . __('An error occured during order confirmation. The error has been logged. Please contact us to determine the status of your order', 'woo-vipps') . "</p>";
5612 5749 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5613 5750 $content .= "</div>";
@@ -5621,93 +5758,21 @@
5621 5758 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5622 5759 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5623 5760 $content .= "</div>";
5624 5761
5762 + return $this->special_page_html('', $content);
5763 + }
5625 5764
5626 - $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5765 + // Returns formatted html for the vipps special page. LP 2026-08-27
5766 + public function special_page_html($header, $content) {
5767 + $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5768 + $html = <<<EOF
5769 + $header_html
5770 + <div class="vipps-special-page-content">$content</div>
5771 + EOF;
5772 + return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5627 5773 }
5628 5774
5629 -
5630 -
5631 - public function fakepage($title,$content) {
5632 - global $wp, $wp_query;
5633 - // We don't want this here.
5634 - remove_filter ('the_content', 'wpautop');
5635 -
5636 - $specialid = $this->gateway()->get_option('vippsspecialpageid');
5637 - $wp_post = null;
5638 - if ($specialid) {
5639 - $wp_post = get_post($specialid);
5640 - if ($wp_post) {
5641 - $wp_post->post_title = $title;
5642 - $wp_post->post_content = $content;
5643 - // Normalize a bit
5644 - $wp_post->filter = 'raw'; // important
5645 - $wp_post->post_status = 'publish';
5646 - $wp_post->comment_status= 'closed';
5647 - $wp_post->ping_status= 'closed';
5648 - } else {
5649 - $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5650 - }
5651 - }
5652 - if (!$wp_post || is_wp_error($wp_post)) {
5653 - $post = new stdClass();
5654 - $post->ID = -99;
5655 - $post->post_author = 1;
5656 - $post->post_date = current_time( 'mysql' );
5657 - $post->post_date_gmt = current_time( 'mysql', 1 );
5658 - $post->post_title = $title;
5659 - $post->post_content = $content;
5660 - $post->post_status = 'publish';
5661 - $post->comment_status = 'closed';
5662 - $post->ping_status = 'closed';
5663 - $post->post_name = 'vippsconfirm-fake-page-name';
5664 - $post->post_type = 'page';
5665 - $post->filter = 'raw'; // important
5666 - $wp_post = new WP_Post($post);
5667 - wp_cache_add( -99, $wp_post, 'posts' );
5668 - }
5669 -
5670 - // Update the main query
5671 - $wp_query->post = $wp_post;
5672 - $wp_query->posts = array( $wp_post );
5673 - $wp_query->queried_object = $wp_post;
5674 - $wp_query->queried_object_id = $wp_post->ID;
5675 - $wp_query->found_posts = 1;
5676 - $wp_query->post_count = 1;
5677 - $wp_query->max_num_pages = 1;
5678 - $wp_query->is_page = true;
5679 - $wp_query->is_singular = true;
5680 - $wp_query->is_single = false;
5681 - $wp_query->is_attachment = false;
5682 - $wp_query->is_archive = false;
5683 - $wp_query->is_category = false;
5684 - $wp_query->is_tag = false;
5685 - $wp_query->is_tax = false;
5686 - $wp_query->is_author = false;
5687 - $wp_query->is_date = false;
5688 - $wp_query->is_year = false;
5689 - $wp_query->is_month = false;
5690 - $wp_query->is_day = false;
5691 - $wp_query->is_time = false;
5692 - $wp_query->is_search = false;
5693 - $wp_query->is_feed = false;
5694 - $wp_query->is_comment_feed = false;
5695 - $wp_query->is_trackback = false;
5696 - $wp_query->is_home = false;
5697 - $wp_query->is_embed = false;
5698 - $wp_query->is_404 = false;
5699 - $wp_query->is_paged = false;
5700 - $wp_query->is_admin = false;
5701 - $wp_query->is_preview = false;
5702 - $wp_query->is_robots = false;
5703 - $wp_query->is_posts_page = false;
5704 - $wp_query->is_post_type_archive = false;
5705 - // Update globals
5706 - $GLOBALS['wp_query'] = $wp_query;
5707 - $wp->register_globals();
5708 - return $wp_post;
5709 - }
5710 5775
5711 5776 // Support the interactivity API with data about our cart IOK 2026-02-23
5712 5777 public function woo_vipps_store_api_cart_data() {
5713 5778 // Reverting the condition with the directive data-wp-bind--hidden does not work, so we need the flipped bool here (hide instead of show). LP 2026-02-10