PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.5
Pay with Vipps and MobilePay for WooCommerce v6.2.5
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 5.4.3 5.4.2 All 187 releases
← All changes | payment/Vipps.class.php +405 -275 6.1.3 → 6.2.5 View file →
@@ -70,9 +70,9 @@
70 70 public static function CheckoutName($order=null) {
71 71 return "Vipps MobilePay Checkout"; // Do not translate
72 72 }
73 73 public static function ExpressCheckoutName($order=null) {
74 - return __("Vipps Express Checkout", 'woo-vipps');
74 + return __("Vipps MobilePay Express Checkout", 'woo-vipps');
75 75 }
76 76 public static function LoginName() {
77 77 return __("Login with Vipps", 'woo-vipps');
78 78 }
@@ -121,12 +121,17 @@
121 121 add_action('init',array($Vipps,'init'));
122 122 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
123 123 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
124 124 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
125 + // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
126 126 // 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 127 // stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25
128 128 add_action('woocommerce_load_shipping_methods', array($Vipps, 'maybe_load_pickup_locations'), 90);
129 +
130 + // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
131 + // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
132 + // is important.
133 + add_filter('woocommerce_create_pages', array($Vipps, 'woocommerce_create_pages'), 50, 1);
129 134 }
130 135
131 136 // Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10
132 137 public function woocommerce_blocks_loaded() {
@@ -280,8 +285,38 @@
280 285 }
281 286
282 287 // Set default button options, migrating any older setup IOK 2026-07-15
283 288 $this->init_button_options();
289 +
290 + /*
291 + From version 6.2.x we create a real physical page to handle the "special" vipps pages,
292 + where we earlier used just a fake page with no real page id, unless especially configured.
293 + We therefore need to add code to maintain this special page.
294 +
295 + woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
296 + and we hook unto this with woocommerce_create_pages. LP 2026-09-03
297 + */
298 +
299 + // Delete special page id option when its deleted or trashed, so that we dont have to load
300 + // in the post to check status in woocommerce_loaded when we ensure the special page exists. LP 2026-09-03
301 + $delete_special_page_id = function($post_id, $post = null) {
302 + if (static::get_special_page_id() === $post_id) {
303 + delete_option('woocommerce_vipps_special_page_page_id');
304 + }
305 + };
306 + add_action('delete_post', $delete_special_page_id, 10, 2);
307 + add_action('wp_trash_post', $delete_special_page_id, 10, 2);
308 +
309 + $this->ensure_special_page_exists();
310 +
311 +
312 + // We want this special page to have a certain title and maybe special scripts and so on,
313 + // this gets run in template redirect for these pages.
314 + add_action('woo_vipps_before_handling_special_page', array($this, 'pre_special_page_actions'));
315 +
316 + // Add an admin interface for this page as well IOK 2026-09-11
317 + add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
318 +
284 319 }
285 320
286 321 public function admin_init () {
287 322 $gw = $this->gateway();
@@ -401,9 +436,77 @@
401 436 }
402 437 }
403 438 }
404 439
440 +
441 + /** Ensure we have a special page for payment flows
442 + *
443 + * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
444 + * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
445 + **/
446 + public function ensure_special_page_exists() {
447 + if (static::get_special_page_id()) return;
448 + $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
405 449
450 + // 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
451 + $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
452 + if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
453 + $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
454 + // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
455 + update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
456 +
457 + // Ensure this page has the necessary shortcode. LP 2026-09-01
458 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
459 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
460 + wp_update_post([
461 + 'ID' => $old_special_page_id,
462 + 'post_content' => $new_content,
463 + ]);
464 + }
465 + } else {
466 + // Create special page if its missing. LP 2026-09-01
467 + $this->maybe_create_vipps_pages();
468 + }
469 + }
470 +
471 + // Admin interface for the special page on woo/advanced/pages
472 + public function woocommerce_settings_pages ($settings) {
473 + $i = -1;
474 + foreach($settings as $entry) {
475 + $i++;
476 + if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
477 + break;
478 + }
479 + }
480 + if ($i > 0) {
481 + $vippspagesettings = array(
482 + array(
483 + 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
484 + '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') ,
485 + 'id' => 'woocommerce_vipps_special_page_page_id',
486 + 'type' => 'single_select_page_with_search',
487 + 'default' => '',
488 + 'class' => 'wc-page-search',
489 + 'css' => 'min-width:300px;',
490 + 'args' => array(
491 + 'exclude' =>
492 + array(
493 + wc_get_page_id( 'myaccount' ),
494 + wc_get_page_id( 'checkout' ),
495 + wc_get_page_id( 'cart' ),
496 + ),
497 + ),
498 + 'desc_tip' => true,
499 + 'autoload' => false,
500 + ));
501 + array_splice($settings, $i, 0, $vippspagesettings);
502 + }
503 +
504 + return $settings;
505 + }
506 +
507 +
508 +
406 509 // Runs on init, adds the Vipps badge feature if activated
407 510 public function maybe_add_vipps_badge_feature () {
408 511 $badge_options = get_option('vipps_badge_options');
409 512 if (!$badge_options || !@$badge_options['badgeon']) return false;
@@ -732,8 +835,11 @@
732 835
733 836 // Get current brand and language
734 837 $current_brand = strtolower($this->get_payment_method_name());
735 838 $current_language = $this->get_customer_language();
839 + if ('se' === $current_language) $current_language = 'sv';
840 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
841 + if ('dk' === $current_language) $current_language = 'da';
736 842
737 843 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
738 844 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
739 845 'purple'=> __('Purple', 'woo-vipps')];
@@ -937,12 +1043,30 @@
937 1043
938 1044 public function get_html_button_attrs_for_context($context = 'global') {
939 1045 $options = get_option('vipps_button_options2', []);
940 1046 if (!is_string($context)) $context = 'global';
1047 +
1048 + // Gutenberg express checkout buttons really want to be stretched, so we'll treat them somewhat differently.
1049 + $gutenberg = false;
1050 + if ($context == 'checkout_gutenberg') {
1051 + $context = 'checkout';
1052 + $gutenberg = true;
1053 + }
1054 + if ($context == 'cart_gutenberg') {
1055 + $context = 'cart';
1056 + $gutenberg = true;
1057 + }
1058 +
941 1059 $config = $options['express']['configs'][$context] ?? [];
942 - if (!$config || ($config['use-global-config'] ?? false)) {
1060 + $use_global = !$config || ($config['use-global-config'] ?? false);
1061 + if ($use_global) {
943 1062 $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
944 1063 }
1064 +
1065 + // see above.
1066 + if ($gutenberg) {
1067 + $config['stretched']='true';
1068 + }
945 1069 return $config;
946 1070 }
947 1071
948 1072 public function get_html_button_for_context($context = 'global') {
@@ -1029,8 +1153,10 @@
1029 1153 private function button_menu_express_section() {
1030 1154 $options = get_option('vipps_button_options2', []);
1031 1155 $express = $options['express'] ?? [];
1032 1156 $configs = $express['configs'] ?? [];
1157 +
1158 +
1033 1159 $contexts = [
1034 1160 'global' => __('Global', 'woo-vipps'),
1035 1161 'product' => __('Product', 'woo-vipps'),
1036 1162 'catalog' => __('Catalog', 'woo-vipps'),
@@ -1161,9 +1287,9 @@
1161 1287
1162 1288 // Update the preview web component's attributes. LP 2026-06-24
1163 1289 function updatePreview(event) {
1164 1290 const args = getPreviewArgs();
1165 - // 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
1291 + // 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
1166 1292 // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1167 1293 if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1168 1294 const button = jQuery('#vipps-button-express-preview');
1169 1295 button.attr(args);
@@ -1203,8 +1329,9 @@
1203 1329
1204 1330 // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1205 1331 const newContext = jQuery("#context").val();
1206 1332 const newConfig = contextConfigs[newContext];
1333 +
1207 1334 setInputsFromConfig(newContext, newConfig);
1208 1335 currentContext = newContext;
1209 1336 }
1210 1337
@@ -1656,9 +1783,9 @@
1656 1783 [
1657 1784 'in_footer' => true,
1658 1785 'strategy' => 'async',
1659 1786 ],
1660 - );
1787 + );
1661 1788
1662 1789 // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1663 1790 wp_register_script('vipps-button-webcomponent',
1664 1791 plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
@@ -1664,10 +1791,9 @@
1664 1791 plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1665 1792 array(),
1666 1793 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1667 1794 [
1668 - 'in_footer' => true,
1669 - 'strategy' => 'async',
1795 + 'in_footer' => false
1670 1796 ],
1671 1797 );
1672 1798 }
1673 1799
@@ -1702,8 +1828,11 @@
1702 1828 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1703 1829 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1704 1830 // Legacy vipps-badge shortcode. LP 19.11.2024
1705 1831 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1832 +
1833 + // special page handling, previously a fake page. LP 2026-08-25
1834 + add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1706 1835 }
1707 1836
1708 1837
1709 1838 public function log ($what,$type='info') {
@@ -1729,8 +1858,10 @@
1729 1858 }
1730 1859
1731 1860 // Show express button option on checkout form. LP 2026-03-23
1732 1861 public function checkout_before_customer_details_express () {
1862 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1863 + do_action('woo_vipps_checkout_before_customer_details_express');
1733 1864 $gw = $this->gateway();
1734 1865 if (!$gw->show_express_checkout()) return;
1735 1866 $this->express_checkout_section_html();
1736 1867 }
@@ -1801,16 +1932,15 @@
1801 1932 public function minicart_express_checkout_button() {
1802 1933 $gw = $this->gateway();
1803 1934
1804 1935 if ($gw->show_express_checkout()){
1805 - return $this->cart_express_checkout_button_html(true);
1936 + return $this->cart_express_checkout_button_html('minicart');
1806 1937 }
1807 1938 }
1808 1939
1809 - public function cart_express_checkout_button_html($minicart = false) {
1940 + public function cart_express_checkout_button_html($context= 'cart') {
1810 1941 $url = $this->express_checkout_url();
1811 1942 $url = wp_nonce_url($url,'express','sec');
1812 - $context = $minicart ? 'minicart' : 'cart';
1813 1943 $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1814 1944 $method = $this->get_payment_method_name();
1815 1945 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1816 1946 $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
@@ -1853,9 +1983,9 @@
1853 1983 public function express_checkout_button_shortcode() {
1854 1984 $gw = $this->gateway();
1855 1985 if (!$gw->cart_supports_express_checkout()) return;
1856 1986 ob_start();
1857 - $this->cart_express_checkout_button_html('shortcode');
1987 + $this->cart_express_checkout_button_html('cart');
1858 1988 return ob_get_clean();
1859 1989 }
1860 1990 // 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
1861 1991 public function express_checkout_banner_shortcode() {
@@ -2516,77 +2646,87 @@
2516 2646 return null;
2517 2647 }
2518 2648 }
2519 2649
2650 + // Special pages, and some callbacks. IOK 2018-05-18
2651 + public function template_redirect() {
2520 2652
2521 - // If this is a special page, return true very early because we are handling this. IOK 2023-02-22
2522 - public function pre_handle_404($current, $query) {
2523 - if (!is_admin()) {
2524 - $special = $this->is_special_page();
2525 - if ($special) {
2526 - // Ensure very early on that Autooptimize does not try to optimize us (if installed) IOK 2023-03-04
2527 - add_filter( 'autoptimize_filter_noptimize', '__return_true');
2528 - return true;
2529 - }
2653 + // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2654 + // We redirect these to the new location.
2655 + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2656 + if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2657 + $url = static::get_special_page_url();
2658 + $_GET['action'] = 'buy_product';
2659 + $q = build_query($_GET);
2660 + wp_redirect($url . "?" . $q, 302);
2661 + exit();
2530 2662 }
2531 - return $current;
2663 +
2664 + if (static::is_special_page()) {
2665 + // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2666 + remove_filter('template_redirect', 'redirect_canonical', 10);
2667 + // dont cache special page. LP 2026-08-25
2668 + $this->nocache();
2669 + // Do the custom pre-load actions for these pages IOK 2026-09-11
2670 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2671 + }
2532 2672 }
2533 2673
2534 - // Special pages, and some callbacks. IOK 2018-05-18
2535 - public function template_redirect() {
2536 - global $post;
2537 - // Handle special callbacks
2538 - $special = $this->is_special_page() ;
2674 + // Ran in template redirect for the special page. IOK 2026-09-2
2675 + public function pre_special_page_actions ($action) {
2676 + // Change title dynamically depending on action. LP 2026-09-02
2677 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2539 2678
2540 - if ($special) {
2541 - remove_filter('template_redirect', 'redirect_canonical', 10);
2542 - do_action('woo_vipps_before_handling_special_page', $special);
2679 + // If we are handling the 'wait for payment' action, we need to poll the order status before
2680 + // we start producing content IOK 2026-09-21
2681 + if ($action == 'wait_for_payment') {
2682 + $this->handle_payment_poll_and_redirect();
2683 + }
2543 2684
2544 - // Allow above hook to actually handle special pages. It should probably call $Vipps->fakepage or a redirect; can be used
2545 - // to intercept express checkout etc. IOK 2022-03-18
2546 - if (! apply_filters('woo_vipps_special_page_handled', false, $special)) {
2547 - $this->$special();
2548 - }
2685 + // Some validation is required for this action
2686 + if ($action == 'do_express_checkout') {
2687 + $this->vipps_express_checkout_consistency_check();
2549 2688 }
2689 + }
2550 2690
2551 - $consentremoval = $this->is_consent_removal();
2552 - if ($consentremoval) {
2553 - remove_filter('template_redirect', 'redirect_canonical', 10);
2554 - do_action('woo_vipps_before_handling_special_page', 'consentremoval');
2555 - if (! apply_filters('woo_vipps_special_page_handled', false, 'consentremoval')) {
2556 - $this->vipps_consent_removal_callback($consentremoval);
2691 +
2692 + // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2693 + public function vipps_special_page_endpoint_title($title, $postid = 0) {
2694 + global $wp_query;
2695 + // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2696 +
2697 + // In block themes the whole template (header, footer, content) renders inside the main
2698 + // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2699 + // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2700 + // the queried page so an earlier title doesn't consume this one-shot filter.
2701 + if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2702 + switch ($_GET['action'] ?? '') {
2703 + case 'wait_for_payment':
2704 + $title = __('Processing order', 'woo-vipps');
2705 + break;
2706 + case 'do_express_checkout':
2707 + case 'buy_product':
2708 + $title = __('Express Checkout', 'woo-vipps');
2709 + break;
2557 2710 }
2558 2711 }
2712 + return $title;
2559 2713 }
2714 +
2560 2715 // Template handling for special pages. IOK 2018-11-21
2716 + // 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
2561 2717 public function template_include($template) {
2562 - $special = $this->is_special_page() ;
2563 - if ($special) {
2718 + if (static::is_special_page()) {
2564 2719 // Get any special template override from the options IOK 2020-02-18
2565 2720 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2566 2721 $found = locate_template($specific,false,false);
2567 2722 if ($found) $template=$found;
2568 2723
2569 - return apply_filters('woo_vipps_special_page_template', $template, $special);
2724 + return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2570 2725 }
2571 2726 return $template;
2572 2727 }
2573 2728
2574 -
2575 - // Can't use wc-api for this, as that does not support DELETE . IOK 2018-05-18
2576 - private function is_consent_removal () {
2577 -
2578 - if ($_SERVER['REQUEST_METHOD'] != 'DELETE') return false;
2579 - if ( !get_option('permalink_structure')) {
2580 - if (@$_REQUEST['vipps-consent-removal']) return @$_REQUEST['callback'];
2581 - return false;
2582 - }
2583 - if (preg_match("!/vipps-consent-removal/([^/]*)!", $_SERVER['REQUEST_URI'], $matches)) {
2584 - return @$_REQUEST['callback'];
2585 - }
2586 - return false;
2587 - }
2588 -
2589 2729 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2590 2730 // the user if using Express Checkout IOK 2020-10-09
2591 2731 public function woocommerce_before_thankyou ($orderid) {
2592 2732 $order = wc_get_order($orderid);
@@ -2591,9 +2731,9 @@
2591 2731 public function woocommerce_before_thankyou ($orderid) {
2592 2732 $order = wc_get_order($orderid);
2593 2733 if ($order) {
2594 2734 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2595 - // -- 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
2735 + // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2596 2736 $this->maybe_log_in_user($order);
2597 2737 $order->delete_meta_data('_vipps_limited_session');
2598 2738 $order->save();
2599 2739
@@ -2654,9 +2794,8 @@
2654 2794
2655 2795 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2656 2796 // IOK 2025-11-19
2657 2797 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2658 -
2659 2798 }
2660 2799
2661 2800 public function get_payment_method_name() {
2662 2801 return $this->gateway()->get_option('payment_method_name');
@@ -2676,14 +2815,13 @@
2676 2815 public function after_setup_theme() {
2677 2816 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2678 2817 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2679 2818
2680 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2819 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2681 2820 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2682 2821 // is important.
2683 2822 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2684 2823
2685 -
2686 2824 // Callbacks use the Woo API IOK 2018-05-18
2687 2825 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2688 2826 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2689 2827
@@ -2694,9 +2832,9 @@
2694 2832 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2695 2833 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2696 2834
2697 2835 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2698 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2836 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2699 2837 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2700 2838
2701 2839 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2702 2840 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
@@ -2701,12 +2839,10 @@
2701 2839 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2702 2840 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2703 2841
2704 2842
2705 - // Special pages and callbacks handled by template_redirect
2706 - // We must also notify WP and other plugins that we are handling this 404-like situation. IOK 2023-02-22
2843 + // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2707 2844 add_action('template_redirect', array($this,'template_redirect'),1);
2708 - add_action('pre_handle_404', array($this, 'pre_handle_404'), 1, 2);
2709 2845
2710 2846 // Allow overriding their templates
2711 2847 add_filter('template_include', array($this,'template_include'), 10, 1);
2712 2848
@@ -2992,14 +3128,14 @@
2992 3128
2993 3129 $raw_post = @file_get_contents( 'php://input' );
2994 3130 $result = @json_decode($raw_post,true);
2995 3131
2996 - // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
3132 + // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
2997 3133 // .. and the epayment webhooks 2023-12-19
2998 3134 $ischeckout = false;
2999 3135 $iswebhook = false;
3000 3136 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
3001 - // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
3137 + // For Checkout v3 and onwards, we control the callback so the type is just this field
3002 3138 if ($callback == 'checkout') {
3003 3139 $ischeckout = true;
3004 3140 }
3005 3141 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
@@ -3413,10 +3549,10 @@
3413 3549 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3414 3550 exit();
3415 3551 }
3416 3552
3417 - // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3418 - // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3553 + // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3554 + // special support for Checkout that this is in fact happening. IOK 2023-01-19
3419 3555 // This needs to be done before "calculate totals".
3420 3556 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3421 3557 $ischeckout = $order->get_meta('_vipps_checkout');
3422 3558
@@ -3592,9 +3728,9 @@
3592 3728 ), 'debug');
3593 3729
3594 3730 }
3595 3731
3596 - // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3732 + // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3597 3733 // 'dynamic pricing' 2023-01-26
3598 3734 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3599 3735 $taxrate = 0;
3600 3736 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
@@ -3720,9 +3856,9 @@
3720 3856 $vippsmethod['shippingMethod'] = $rate->get_label();
3721 3857 $vippsmethod['shippingMethodId'] = $key;
3722 3858 $vippsmethods[]=$vippsmethod;
3723 3859
3724 - // Metadata and settings stored for later use for Vipps Checkout
3860 + // Metadata and settings stored for later use for Checkout
3725 3861 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3726 3862 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3727 3863 // finalize the order. IOK 2025-08-15
3728 3864 $ratemap[$key]=$rate;
@@ -3740,9 +3876,9 @@
3740 3876 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3741 3877 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3742 3878 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3743 3879
3744 - // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3880 + // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3745 3881 // IOK 2025-08-15 and new Express yet another slightly different format.
3746 3882 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3747 3883 if ($ischeckout) {
3748 3884 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
@@ -4032,9 +4168,9 @@
4032 4168 WC()->cart->calculate_totals();
4033 4169 WC()->cart->set_session();
4034 4170 return true;
4035 4171 } catch (Exception $e) {
4036 - $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error');
4172 + $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->getMessage()), 'error');
4037 4173 return false;
4038 4174 }
4039 4175 }
4040 4176
@@ -4118,17 +4254,8 @@
4118 4254 header("X-Accel-Expires: 0");
4119 4255 }
4120 4256
4121 4257
4122 -
4123 - // Handle DELETE on a vipps consent removal callback
4124 - public function vipps_consent_removal_callback ($callback) {
4125 - Vipps::nocache();
4126 - // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
4127 - // to escape here when the API is changed. IOK 2020-10-14
4128 - $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
4129 - }
4130 -
4131 4258 public function woocommerce_payment_gateways($methods) {
4132 4259 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4133 4260 require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
4134 4261 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
@@ -4153,9 +4280,11 @@
4153 4280 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
4154 4281 return $url;
4155 4282 }
4156 4283 $url = $this->express_checkout_url();
4157 - $url = wp_nonce_url($url,'express','sec');
4284 + // At this point, there is always a query argument here. IOK 2026-09-21
4285 + $nonce = wp_create_nonce('express');
4286 + $url = $url . "&sec=$nonce";
4158 4287
4159 4288 return $url;
4160 4289 }
4161 4290
@@ -4222,9 +4351,9 @@
4222 4351 // Poll status and correct woo status. LP 2026-05-19
4223 4352 $order_data = $gw->get_payment_details($order);
4224 4353
4225 4354 // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4226 - if ('CANCEL' === $order_data['STATE']) {
4355 + if ('CANCEL' === ($order_data['state'] ?? "")) {
4227 4356 /* translators: company name */
4228 4357 $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4229 4358 return;
4230 4359 }
@@ -4251,20 +4380,40 @@
4251 4380 }
4252 4381 }
4253 4382
4254 4383 public function activate () {
4255 - static::maybe_add_cron_event();
4256 - $gw = $this->gateway();
4384 + static::maybe_add_cron_event();
4385 + $gw = $this->gateway();
4257 4386
4258 - // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4259 - if ($gw->get_option('orderprefix') == 'Woo') {
4260 - $gw->update_option('orderprefix', $this->generate_order_prefix());
4261 - }
4262 - // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4263 - $gw->initialize_webhooks();
4264 - $this->payment_method_name = $gw->get_option('payment_method_name');
4265 - }
4387 + // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4388 + if ($gw->get_option('orderprefix') == 'Woo') {
4389 + $gw->update_option('orderprefix', $this->generate_order_prefix());
4390 + }
4391 + // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4392 + $gw->initialize_webhooks();
4393 + $this->payment_method_name = $gw->get_option('payment_method_name');
4266 4394
4395 +
4396 + // Check if the special page is noted and actually does exist
4397 + $special = static::get_special_page_id();
4398 + if ($special) {
4399 + $special_page = get_post($special);
4400 + if ($special_page && 'trash' !== $special_page->post_status) {
4401 + // Ensure this page has the necessary shortcode. LP 2026-09-01
4402 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4403 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4404 + wp_update_post([
4405 + 'ID' => $special,
4406 + 'post_content' => $new_content,
4407 + ]);
4408 + }
4409 + } else {
4410 + delete_option('woocommerce_vipps_special_page_page_id');
4411 + }
4412 + }
4413 +
4414 + }
4415 +
4267 4416 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4268 4417 public static function deactivate() {
4269 4418 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4270 4419 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
@@ -4317,9 +4466,10 @@
4317 4466 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4318 4467 private function maybe_set_vipps_as_default() {
4319 4468 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4320 4469 $gw = $this->gateway();
4321 - if ($gw->get_option('vippsdefault')=='yes') {
4470 + // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4471 + if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4322 4472 WC()->session->set('chosen_payment_method', $gw->id);
4323 4473 }
4324 4474 }
4325 4475
@@ -4424,13 +4574,13 @@
4424 4574 if (is_user_logged_in()) return;
4425 4575 if (!$order || ! self::is_vipps_order($order)) return;
4426 4576
4427 4577 // We *do* want to log in express checkout customers, but not those that
4428 - // use the Vipps Checkout solution - those can change their emails in the
4578 + // use the Checkout solution - those can change their emails in the
4429 4579 // checkout screen. IOK 2021-09-03
4430 4580 $do_login = $order->get_meta('_vipps_express_checkout');
4431 4581
4432 - // We will not log in Vipps Checkout users unless the option for that is true
4582 + // We will not log in Checkout users unless the option for that is true
4433 4583 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4434 4584 $do_login = false;
4435 4585 }
4436 4586
@@ -4465,9 +4615,9 @@
4465 4615
4466 4616 // Both Checkout and Express Checkout have the below value set to true
4467 4617 if (!$order->get_meta('_vipps_express_checkout')) return;
4468 4618
4469 - // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4619 + // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4470 4620 // IOK 2023-07-27
4471 4621 $ischeckout = $order->get_meta('_vipps_checkout');
4472 4622 if ($ischeckout) {
4473 4623 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
@@ -4567,9 +4717,10 @@
4567 4717 }
4568 4718 if (!$o) return;
4569 4719 if (!$o->get_meta('_vipps_single_product_express')) return;
4570 4720 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4571 - if ($failed) WC()->cart->empty_cart();
4721 + // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4722 + WC()->cart->empty_cart();
4572 4723 $this->restore_cart($o);
4573 4724 }
4574 4725
4575 4726
@@ -4852,9 +5003,9 @@
4852 5003 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4853 5004 }
4854 5005 }
4855 5006
4856 - // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
5007 + // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4857 5008 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4858 5009 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4859 5010 // 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
4860 5011 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
@@ -4925,46 +5076,37 @@
4925 5076 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4926 5077 return false;
4927 5078 }
4928 5079
4929 - // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4930 - // These are for the "fallback URL" mostly. IOK 2018-05-18
4931 - private function make_vipps_url($what) {
4932 - if ( !get_option('permalink_structure')) {
4933 - return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4934 - }
4935 - return trailingslashit(home_url($what, 'https'));
5080 + // 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
5081 + private function make_special_page_url($action) {
5082 + return add_query_arg('action', $action, $this->get_special_page_url());
4936 5083 }
5084 +
4937 5085 public function payment_return_url() {
4938 - return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
5086 + return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
4939 5087 }
4940 5088 public function express_checkout_url() {
4941 - return $this->make_vipps_url('vipps-express-checkout');
5089 + return $this->make_special_page_url('do_express_checkout');
4942 5090 }
4943 5091 public function buy_product_url() {
4944 - return $this->make_vipps_url('vipps-buy-product');
5092 + return $this->make_special_page_url('buy_product');
4945 5093 }
4946 5094
4947 - // Return the method in the Vipps
4948 - public function is_special_page() {
4949 - $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4950 - $method = null;
4951 - if ( get_option('permalink_structure')) {
4952 - foreach($specials as $special=>$specialmethod) {
4953 - // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4954 - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4955 - if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4956 - $method = $specialmethod; break;
4957 - }
4958 - }
4959 - } else {
4960 - if (isset($_GET['VippsSpecialPage'])) {
4961 - $method = @$specials[$_GET['VippsSpecialPage']];
4962 - }
4963 - }
4964 - return $method;
5095 + public static function is_special_page() {
5096 + $id = static::get_special_page_id();
5097 + return $id && is_page($id);
4965 5098 }
4966 5099
5100 + public static function get_special_page_id() {
5101 + $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5102 + return $id > 0 ? $id : null;
5103 + }
5104 +
5105 + public static function get_special_page_url() {
5106 + return get_permalink(static::get_special_page_id());
5107 + }
5108 +
4967 5109 // Just create a spinner and a overlay.
4968 5110 public function spinner () {
4969 5111 $flavour = sanitize_title($this->get_payment_method_name());
4970 5112 ob_start();
@@ -5011,16 +5153,8 @@
5011 5153 }
5012 5154 return null;
5013 5155 }
5014 5156
5015 - // DEPRECATED: Legacy function as of new web component express buttons. see get_buy_now_button and get_html_button. LP 2026-06-26
5016 - public function get_buy_now_button_manual($product_id, $variation_id=null, $sku=null, $disabled=false, $classes='',
5017 - $_logo_variant=null, $_logo_lang=null, // deprecated params
5018 - $context='global', $button_args_override = [],
5019 - ) {
5020 - return $this->get_buy_now_button($product_id, $variation_id, $sku, $disabled, $classes, $context, $button_args_override);
5021 - }
5022 -
5023 5157 // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5024 5158 // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5025 5159 // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5026 5160 public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
@@ -5064,8 +5198,11 @@
5064 5198 if ($classes) $classes = " $classes";
5065 5199 if ($short) $classes = "short $classes";
5066 5200
5067 5201 $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5202 +
5203 +
5204 +
5068 5205 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
5069 5206 }
5070 5207
5071 5208 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
@@ -5149,43 +5286,90 @@
5149 5286 }
5150 5287
5151 5288
5152 5289
5153 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5290 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5154 5291 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5292 + // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5155 5293 public function woocommerce_create_pages ($data) {
5294 + // Vipps Checkout page
5156 5295 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5157 - if (!$vipps_checkout_activated) return $data;
5296 + if ($vipps_checkout_activated) {
5297 + $data['vipps_checkout'] = array(
5298 + 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5299 + 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5300 + 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5301 + );
5302 + }
5158 5303
5159 - $data['vipps_checkout'] = array(
5160 - 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5161 - 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5162 - 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5163 - );
5164 -
5304 + // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5305 + $data['vipps_special_page'] = [
5306 + 'name' => 'vipps-payment', // slug
5307 + /* translators: company name */
5308 + 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5309 + 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5310 + ];
5165 5311 return $data;
5166 5312 }
5167 5313
5168 - // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5314 + // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5315 + // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5169 5316 public function maybe_create_vipps_pages () {
5317 + $make_pages = false;
5318 +
5319 + // Vipps Checkout page. LP 2026-08-18
5170 5320 $checkoutid = wc_get_page_id('vipps_checkout');
5171 - $makeit = !$checkoutid || ! get_post_status($checkoutid);
5172 - if ($makeit) {
5321 + if (!$checkoutid || ! get_post_status($checkoutid)) {
5173 5322 delete_option('woocommerce_vipps_checkout_page_id');
5323 + $make_pages = true;
5174 5324 }
5175 5325
5176 - if ($makeit) {
5177 - WC_Install::create_pages();
5326 + // vipps special page, previously a fake page. LP 2026-08-18
5327 + $builtin_special_page_id = static::get_special_page_id();
5328 + if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5329 + delete_option('woocommerce_vipps_special_page_page_id');
5330 + $make_pages = true;
5178 5331 }
5332 +
5333 + if ($make_pages) {
5334 + WC_Install::create_pages();
5335 + }
5179 5336 }
5180 5337
5338 + public function vipps_special_page_shortcode($atts, $content) {
5339 + // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5340 + if (is_admin()) return;
5341 + if (wp_doing_ajax()) return;
5342 + if (defined('REST_REQUEST') && REST_REQUEST) return;
5343 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5181 5344
5345 + $action = $_GET['action'] ?? '';
5346 + $html = "";
5347 + switch ($action) {
5348 + case 'wait_for_payment':
5349 + $html = $this->vipps_wait_for_payment();
5350 + break;
5351 + case 'do_express_checkout':
5352 + $html = $this->vipps_express_checkout();
5353 + break;
5354 + case 'buy_product':
5355 + $html = $this->vipps_buy_product();
5356 + break;
5357 + default:
5358 + $html = '';
5359 + }
5360 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5361 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5362 +
5363 + // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5364 + return $html;
5365 + }
5366 +
5367 +
5182 5368 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5183 5369 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5184 5370 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5185 5371 public function vipps_buy_product() {
5186 - status_header(200,'OK');
5187 - Vipps::nocache();
5188 5372
5189 5373 add_filter('body_class', function ($classes) {
5190 5374 $classes[] = 'vipps-express-checkout';
5191 5375 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
@@ -5221,9 +5405,9 @@
5221 5405
5222 5406 if (!$productinfo) {
5223 5407 $title = __("Product is no longer available",'woo-vipps');
5224 5408 $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');
5225 - return $this->fakepage($title,$content);
5409 + return $this->special_page_html($title,$content);
5226 5410 }
5227 5411
5228 5412 // Pass the productinfo to the express checkout form
5229 5413 $args = array();
@@ -5240,20 +5424,16 @@
5240 5424 }
5241 5425 $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5242 5426 }
5243 5427
5244 - $this->print_express_checkout_page(true,'do_single_product_express_checkout',$args);
5428 + return $this->express_checkout_page_html(true,'do_single_product_express_checkout',$args);
5245 5429 }
5246 5430
5247 - // 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.
5248 - public function vipps_express_checkout() {
5249 - status_header(200,'OK');
5250 - Vipps::nocache();
5431 + public function vipps_express_checkout_consistency_check() {
5251 5432 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5252 5433 // IOK 2018-05-28
5253 5434 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5254 5435
5255 -
5256 5436 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5257 5437 if (!$backurl) $backurl = home_url();
5258 5438
5259 5439 if (!$ok) {
@@ -5267,8 +5447,19 @@
5267 5447 wp_redirect($backurl);
5268 5448 exit();
5269 5449 }
5270 5450
5451 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5452 + }
5453 +
5454 + // 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.
5455 + public function vipps_express_checkout() {
5456 + // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5457 + if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5458 + $content = __('Link expired, please try again', 'woo-vipps');
5459 + return $content;
5460 + }
5461 +
5271 5462 add_filter('body_class', function ($classes) {
5272 5463 $classes[] = 'vipps-express-checkout';
5273 5464 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5274 5465 return apply_filters('woo_vipps_express_checkout_body_class', $classes);
@@ -5275,9 +5466,9 @@
5275 5466 });
5276 5467
5277 5468 do_action('woo_vipps_express_checkout_page');
5278 5469
5279 - $this->print_express_checkout_page(true, 'do_express_checkout');
5470 + return $this->express_checkout_page_html(true, 'do_express_checkout');
5280 5471 }
5281 5472
5282 5473 // This method tries to ensure that a customer does not 'lose' the return page and
5283 5474 // starts ordering the same products twice. IOK 2020-01-22
@@ -5372,9 +5563,10 @@
5372 5563 return $orderspec;
5373 5564 }
5374 5565
5375 5566 // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5376 - protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5567 + // Returns the html. LP 2026-08-27
5568 + protected function express_checkout_page_html($execute,$action,$productinfo=null) {
5377 5569 $gw = $this->gateway();
5378 5570
5379 5571 $expressCheckoutMessages = array();
5380 5572 $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
@@ -5457,10 +5649,9 @@
5457 5649
5458 5650 if ($execute) {
5459 5651 $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5460 5652 $content .= "<div id='vipps-status-message'></div>";
5461 - $this->fakepage(__('Order in progress','woo-vipps'), $content);
5462 - return;
5653 + return $this->special_page_html('', $content);
5463 5654 } else {
5464 5655 $content .= $askForConfirmationHTML;
5465 5656 $content .= $extraHTML;
5466 5657 $content .= $termsHTML;
@@ -5467,21 +5658,16 @@
5467 5658 $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5468 5659 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5469 5660 $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>";
5470 5661 $content .= "<div id='vipps-status-message'></div>";
5471 - $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5472 - return;
5662 + return $this->special_page_html('', $content);
5473 5663 }
5474 5664 }
5475 5665
5476 5666
5477 -
5478 - public function vipps_wait_for_payment() {
5479 - status_header(200,'OK');
5480 - Vipps::nocache();
5481 -
5667 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5668 + private function handle_payment_poll_and_redirect () {
5482 5669 $orderid = WC()->session->get('_vipps_pending_order');
5483 -
5484 5670 $order = null;
5485 5671 $gw = $this->gateway();
5486 5672
5487 5673 // Failsafe for when the session disappears IOK 2018-11-19
@@ -5493,9 +5679,9 @@
5493 5679 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5494 5680 // simulating the session with that.
5495 5681 // IOK 2019-11-19, changed to using GET 2023-01-23
5496 5682 if ($no_session && $limited_session) {
5497 - $orderid = intval(@$_GET['id']);
5683 + $orderid = intval($_GET['id'] ?? false);
5498 5684 }
5499 5685 if ($orderid) {
5500 5686 clean_post_cache($orderid);
5501 5687 $order = wc_get_order($orderid);
@@ -5514,11 +5700,8 @@
5514 5700 $session->set('_vipps_pending_order', $orderid);
5515 5701 }
5516 5702 }
5517 5703
5518 -
5519 - do_action('woo_vipps_wait_for_payment_page',$order);
5520 -
5521 5704 $deleted_order=0;
5522 5705 if ($orderid && !$order) {
5523 5706 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5524 5707 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
@@ -5543,9 +5726,9 @@
5543 5726 clean_post_cache($orderid);
5544 5727 $order = wc_get_order($orderid); // Reload order object
5545 5728 }
5546 5729 } else {
5547 - // No need to do anyting here. IOK 2020-01-26
5730 + // No need to do anyting here. IOK 2020-01-26
5548 5731 }
5549 5732
5550 5733 $payment = 'notchecked';
5551 5734 if ($do_poll) {
@@ -5561,9 +5744,8 @@
5561 5744 exit();
5562 5745 }
5563 5746
5564 5747 // We are done, but in failure. Don't poll.
5565 - $content = "";
5566 5748 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5567 5749
5568 5750 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5569 5751 // 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
@@ -5571,8 +5753,9 @@
5571 5753 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5572 5754 wp_redirect($failure_redirect);
5573 5755 exit();
5574 5756 }
5757 +
5575 5758 if ($status == 'cancelled' || $payment == 'cancelled') {
5576 5759 $this->maybe_restore_cart($orderid,'failed');
5577 5760 if ($failure_redirect){
5578 5761 wp_redirect($failure_redirect);
@@ -5577,27 +5760,45 @@
5577 5760 if ($failure_redirect){
5578 5761 wp_redirect($failure_redirect);
5579 5762 exit();
5580 5763 }
5764 + } else {
5765 + // If not, enqueue the status checker IOK 2026-09-21
5766 + 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');
5767 + }
5768 +
5769 + // Communicate this to the shortcode IOK 2026-09-21
5770 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5771 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5772 + });
5773 +
5774 + }
5775 +
5776 + public function vipps_wait_for_payment() {
5777 +
5778 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5779 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5780 +
5781 + $orderid = $data['orderid'] ?? 0;
5782 + $status = $data['status'] ?? "";
5783 + $payment = $data['payment'] ?? "";
5784 +
5785 + $order = wc_get_order($orderid);
5786 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5787 +
5788 + do_action('woo_vipps_wait_for_payment_page',$order);
5789 + $gw = $this->gateway();
5790 +
5791 + $content = "";
5792 + if ($status == 'cancelled' || $payment == 'cancelled') {
5581 5793 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5582 5794 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5583 5795 $content .= "</div>";
5584 - $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5585 -
5586 - return;
5796 + return $this->special_page_html('', $content);
5587 5797 }
5588 5798
5589 5799 // 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
5590 -
5591 5800 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5592 - 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');
5593 -
5594 - // 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
5595 - // and check that the order matches (and is 'pending') (and exists)
5596 - $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5597 - $vippsstatus = $order->get_meta('_vipps_status');
5598 - $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5599 -
5600 5801 $signal = $this->callbackSignal($order);
5601 5802 $content = "";
5602 5803 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5603 5804
@@ -5605,15 +5806,16 @@
5605 5806 $signalurl = $this->callbackSignalURL($signal);
5606 5807
5607 5808 $content .= "</p></div>";
5608 5809
5609 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5610 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5611 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5810 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5811 +
5812 + // Carry the order status to the checking script IOK 2026-09-21
5813 + $content .= "<form id='vippsdata'>";
5612 5814 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5613 5815 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5614 5816 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5615 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5817 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5616 5818 $content .= "</form>";
5617 5819
5618 5820
5619 5821 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
@@ -5630,93 +5832,21 @@
5630 5832 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5631 5833 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5632 5834 $content .= "</div>";
5633 5835
5836 + return $this->special_page_html('', $content);
5837 + }
5634 5838
5635 - $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5839 + // Returns formatted html for the vipps special page. LP 2026-08-27
5840 + public function special_page_html($header, $content) {
5841 + $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5842 + $html = <<<EOF
5843 + $header_html
5844 + <div class="vipps-special-page-content">$content</div>
5845 + EOF;
5846 + return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5636 5847 }
5637 5848
5638 -
5639 -
5640 - public function fakepage($title,$content) {
5641 - global $wp, $wp_query;
5642 - // We don't want this here.
5643 - remove_filter ('the_content', 'wpautop');
5644 -
5645 - $specialid = $this->gateway()->get_option('vippsspecialpageid');
5646 - $wp_post = null;
5647 - if ($specialid) {
5648 - $wp_post = get_post($specialid);
5649 - if ($wp_post) {
5650 - $wp_post->post_title = $title;
5651 - $wp_post->post_content = $content;
5652 - // Normalize a bit
5653 - $wp_post->filter = 'raw'; // important
5654 - $wp_post->post_status = 'publish';
5655 - $wp_post->comment_status= 'closed';
5656 - $wp_post->ping_status= 'closed';
5657 - } else {
5658 - $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5659 - }
5660 - }
5661 - if (!$wp_post || is_wp_error($wp_post)) {
5662 - $post = new stdClass();
5663 - $post->ID = -99;
5664 - $post->post_author = 1;
5665 - $post->post_date = current_time( 'mysql' );
5666 - $post->post_date_gmt = current_time( 'mysql', 1 );
5667 - $post->post_title = $title;
5668 - $post->post_content = $content;
5669 - $post->post_status = 'publish';
5670 - $post->comment_status = 'closed';
5671 - $post->ping_status = 'closed';
5672 - $post->post_name = 'vippsconfirm-fake-page-name';
5673 - $post->post_type = 'page';
5674 - $post->filter = 'raw'; // important
5675 - $wp_post = new WP_Post($post);
5676 - wp_cache_add( -99, $wp_post, 'posts' );
5677 - }
5678 -
5679 - // Update the main query
5680 - $wp_query->post = $wp_post;
5681 - $wp_query->posts = array( $wp_post );
5682 - $wp_query->queried_object = $wp_post;
5683 - $wp_query->queried_object_id = $wp_post->ID;
5684 - $wp_query->found_posts = 1;
5685 - $wp_query->post_count = 1;
5686 - $wp_query->max_num_pages = 1;
5687 - $wp_query->is_page = true;
5688 - $wp_query->is_singular = true;
5689 - $wp_query->is_single = false;
5690 - $wp_query->is_attachment = false;
5691 - $wp_query->is_archive = false;
5692 - $wp_query->is_category = false;
5693 - $wp_query->is_tag = false;
5694 - $wp_query->is_tax = false;
5695 - $wp_query->is_author = false;
5696 - $wp_query->is_date = false;
5697 - $wp_query->is_year = false;
5698 - $wp_query->is_month = false;
5699 - $wp_query->is_day = false;
5700 - $wp_query->is_time = false;
5701 - $wp_query->is_search = false;
5702 - $wp_query->is_feed = false;
5703 - $wp_query->is_comment_feed = false;
5704 - $wp_query->is_trackback = false;
5705 - $wp_query->is_home = false;
5706 - $wp_query->is_embed = false;
5707 - $wp_query->is_404 = false;
5708 - $wp_query->is_paged = false;
5709 - $wp_query->is_admin = false;
5710 - $wp_query->is_preview = false;
5711 - $wp_query->is_robots = false;
5712 - $wp_query->is_posts_page = false;
5713 - $wp_query->is_post_type_archive = false;
5714 - // Update globals
5715 - $GLOBALS['wp_query'] = $wp_query;
5716 - $wp->register_globals();
5717 - return $wp_post;
5718 - }
5719 5849
5720 5850 // Support the interactivity API with data about our cart IOK 2026-02-23
5721 5851 public function woo_vipps_store_api_cart_data() {
5722 5852 // 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