PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.3.0
Pay with Vipps and MobilePay for WooCommerce v6.3.0
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 6.0.0 All 189 releases
← All changes | payment/Vipps.class.php +592 -512 6.2.0 → 6.3.0 View file →
@@ -117,9 +117,10 @@
117 117 add_action('wp_footer', array($Vipps,'footer'));
118 118 }
119 119 add_action( 'plugins_loaded', array($Vipps,'plugins_loaded'));
120 120 add_action( 'after_setup_theme', array($Vipps,'after_setup_theme'));
121 - add_action('init',array($Vipps,'init'));
121 + add_action( 'init',array($Vipps,'init'));
122 + add_action( 'rest_api_init', array($Vipps, 'rest_api_init'));
122 123 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
123 124 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
124 125 add_action( 'woocommerce_blocks_loaded', [$Vipps, 'woocommerce_blocks_loaded']);
125 126 // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
@@ -227,8 +228,9 @@
227 228 // Register certain scripts in wp_loaded because they will be added to the backend as well - the gutenberg checkout block
228 229 // needs these to be defined in the backend. IOK 2024-04-16
229 230 add_action('wp_loaded', array($this, 'wp_register_scripts'));
230 231 add_action('wp_enqueue_scripts', array($this, 'wp_enqueue_scripts'));
232 + add_action('wp_enqueue_scripts', array($this, 'enqueue_classic_checkout_scripts'), 20);
231 233
232 234 // Remove the possibility of restarting failed orders etc. This will be fixed in the future. IOK 2023-05-26
233 235 add_filter('woocommerce_my_account_my_orders_actions', array($this,'woocommerce_my_account_my_orders_actions'), 10, 2);
234 236
@@ -244,16 +246,8 @@
244 246
245 247 // Extra order actions on the order screen, now using ajax to be compatible with HPOS. IOK 2022-12-02
246 248 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
247 249
248 - // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
249 - add_action('rest_api_init', function() {
250 - register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
251 - 'methods' => 'GET',
252 - 'callback' => [$this, 'rest_express_checkout_products'],
253 - 'permission_callback' => '__return_true',
254 - ]);
255 - });
256 250
257 251 // We need a 5-minute scheduled event for the handler for missed callbacks. Using the
258 252 // action scheduler would be better, but we can't do that just yet because of backwards
259 253 // compatibility. At some point, support for older woo-versions should be dropped; then this
@@ -310,16 +304,36 @@
310 304
311 305
312 306 // We want this special page to have a certain title and maybe special scripts and so on,
313 307 // this gets run in template redirect for these pages.
314 - add_action('woo_vipps_before_handling_special_page', function ($action) {
315 - // Change title dynamically depending on action. LP 2026-09-02
316 - add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
317 - });
308 + add_action('woo_vipps_before_handling_special_page', array($this, 'pre_special_page_actions'));
318 309
319 310 // Add an admin interface for this page as well IOK 2026-09-11
320 311 add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
312 + }
321 313
314 +
315 + public function rest_api_init () {
316 +
317 + // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
318 + register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
319 + 'methods' => 'GET',
320 + 'callback' => [$this, 'rest_express_checkout_products'],
321 + 'permission_callback' => '__return_true',
322 + ]);
323 +
324 + // Start a single product express checkout process. IOK 2026-08-25
325 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout_single', [
326 + 'methods' => 'POST',
327 + 'callback' => [$this, 'rest_do_single_product_express_checkout'],
328 + 'permission_callback' => '__return_true',
329 + ]);
330 + // And one for the cart. IOK 2026-09-04
331 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout', [
332 + 'methods' => 'POST',
333 + 'callback' => [$this, 'rest_do_express_checkout'],
334 + 'permission_callback' => '__return_true',
335 + ]);
322 336 }
323 337
324 338 public function admin_init () {
325 339 $gw = $this->gateway();
@@ -348,10 +362,8 @@
348 362 // Styling etc
349 363 add_action('admin_head', array($this, 'admin_head'));
350 364
351 365 // Scripts
352 - $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
353 - wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
354 366 add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts'));
355 367
356 368 // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
357 369 add_action('current_screen', function ($screen) {
@@ -1046,12 +1058,30 @@
1046 1058
1047 1059 public function get_html_button_attrs_for_context($context = 'global') {
1048 1060 $options = get_option('vipps_button_options2', []);
1049 1061 if (!is_string($context)) $context = 'global';
1062 +
1063 + // Gutenberg express checkout buttons really want to be stretched, so we'll treat them somewhat differently.
1064 + $gutenberg = false;
1065 + if ($context == 'checkout_gutenberg') {
1066 + $context = 'checkout';
1067 + $gutenberg = true;
1068 + }
1069 + if ($context == 'cart_gutenberg') {
1070 + $context = 'cart';
1071 + $gutenberg = true;
1072 + }
1073 +
1050 1074 $config = $options['express']['configs'][$context] ?? [];
1051 - if (!$config || ($config['use-global-config'] ?? false)) {
1075 + $use_global = !$config || ($config['use-global-config'] ?? false);
1076 + if ($use_global) {
1052 1077 $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
1053 1078 }
1079 +
1080 + // see above.
1081 + if ($gutenberg) {
1082 + $config['stretched']='true';
1083 + }
1054 1084 return $config;
1055 1085 }
1056 1086
1057 1087 public function get_html_button_for_context($context = 'global') {
@@ -1138,8 +1168,10 @@
1138 1168 private function button_menu_express_section() {
1139 1169 $options = get_option('vipps_button_options2', []);
1140 1170 $express = $options['express'] ?? [];
1141 1171 $configs = $express['configs'] ?? [];
1172 +
1173 +
1142 1174 $contexts = [
1143 1175 'global' => __('Global', 'woo-vipps'),
1144 1176 'product' => __('Product', 'woo-vipps'),
1145 1177 'catalog' => __('Catalog', 'woo-vipps'),
@@ -1312,8 +1344,9 @@
1312 1344
1313 1345 // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1314 1346 const newContext = jQuery("#context").val();
1315 1347 const newConfig = contextConfigs[newContext];
1348 +
1316 1349 setInputsFromConfig(newContext, newConfig);
1317 1350 currentContext = newContext;
1318 1351 }
1319 1352
@@ -1676,12 +1709,14 @@
1676 1709 <?php
1677 1710 }
1678 1711 // Scripts used in the backend
1679 1712 public function admin_enqueue_scripts($hook) {
1713 +
1714 + wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1715 + $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
1716 + wp_localize_script('vipps-admin', 'VippsConfig', $this->vippsJSConfig);
1680 1717 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1681 - $this->script_add_vippslocale();
1682 -
1683 - wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1718 + $this->script_add_vippslocale('vipps-admin');
1684 1719 wp_enqueue_script('vipps-admin');
1685 1720
1686 1721 wp_enqueue_style('vipps-admin-style',plugins_url('css/admin.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/admin.css"), 'all');
1687 1722 wp_enqueue_style('vipps-fonts');
@@ -1751,12 +1786,9 @@
1751 1786
1752 1787 public function wp_register_scripts () {
1753 1788 // 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
1754 1789 // (which it only is if Gutenberg is active) or not provided at all, add it now.
1755 - if (!wp_script_is( 'wp-hooks', 'registered')) {
1756 - wp_register_script('wp-hooks', plugins_url('/compat/hooks.min.js', __FILE__));
1757 - }
1758 - wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/vipps.js"), 'true');
1790 + 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);
1759 1791
1760 1792 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1761 1793 wp_register_script('vipps-onsite-messageing',
1762 1794 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
@@ -1762,13 +1794,27 @@
1762 1794 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1763 1795 array(),
1764 1796 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1765 1797 [
1766 - 'in_footer' => true,
1767 - 'strategy' => 'async',
1798 + 'in_footer' => true,
1799 + 'strategy' => 'async',
1768 1800 ],
1769 1801 );
1770 1802
1803 + add_filter( 'script_loader_tag', function($tag, $handle,$src) {
1804 + if ($handle == 'vipps-widget-sdk') {
1805 + $tag = preg_replace("!^<script!", "<script data-vipps-widget-sdk ", $tag);
1806 + return $tag;
1807 + }
1808 + return $tag;
1809 + },10,3);
1810 +
1811 + wp_register_script('vipps-widget-sdk', "https://cdn.vippsmobilepay.com/js/widget-sdk/vipps-widget.js",
1812 + array('vipps-button-webcomponent'),
1813 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps.js'),
1814 + ['in_footer' => true]
1815 + );
1816 +
1771 1817 // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1772 1818 wp_register_script('vipps-button-webcomponent',
1773 1819 plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1774 1820 array(),
@@ -1774,27 +1820,45 @@
1774 1820 array(),
1775 1821 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1776 1822 [
1777 1823 'in_footer' => false
1778 - ],
1824 + ]
1779 1825 );
1780 1826 }
1781 1827
1782 1828 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1783 - public function script_add_vippslocale () {
1829 + public function script_add_vippslocale ($handle) {
1784 1830 // This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13
1831 + $name = $this->get_payment_method_name();
1785 1832 $strings = array(
1786 - 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1787 - 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()),
1788 - 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1833 + 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $name),
1834 + 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $name),
1835 + 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $name),
1836 + 'termsAndConditionsError' => __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ),
1837 + 'temporaryError' => sprintf(__('%1$s is temporarily unavailable.','woo-vipps'),$name),
1838 + 'successMessage' => sprintf(__('To the %1$s app!','woo-vipps'), $name),
1839 + 'cancel'=> __("Cancel", 'woo-vipps'),
1840 + 'close'=> __("Close", 'woo-vipps'),
1841 + 'missingPaymentUrl'=> __("Successful checkout response has no payment URL", 'woo-vipps'),
1842 + 'expressCheckoutFailed'=> __("Express checkout failed", 'woo-vipps'),
1843 + 'unexpectedCheckoutResponse'=> __("Unexpected express checkout response", 'woo-vipps'),
1844 + 'vippsCheckoutFailed'=> __("Vipps Mobilepay checkout failed", 'woo-vipps'),
1845 + 'correctHighlightedFields'=> __("Please correct the highlighted fields.", 'woo-vipps'),
1846 + 'checkFormBeforeContinuing'=> __("Please check the form before continuing.", 'woo-vipps'),
1847 + 'cartCheckoutUnavailable'=> __("Cannot start express checkout: cart checkout is unavailable", 'woo-vipps'),
1848 + 'productIdentifiersMissing'=> __("Cannot buy product: product id, variation id and sku are missing", 'woo-vipps'),
1849 + 'productFormNotFound'=> __("Cannot buy product: product form not found", 'woo-vipps'),
1850 + 'paymentSuccessfulRedirecting' => __("Payment successful. Redirecting…", 'woo-vipps'),
1789 1851 );
1790 - wp_localize_script('vipps-gw', 'VippsLocale', $strings);
1852 + wp_localize_script($handle, 'VippsLocale', $strings);
1791 1853 }
1792 1854
1793 1855 public function wp_enqueue_scripts() {
1856 + // Add late: if this value isn't 'yes' we wil not add order attribution to express orders. IOK 2026-09-10
1857 + $this->vippsJSConfig['expressOrderAttribution'] = $this->gateway()->get_option('vippsorderattribution');
1794 1858 wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
1795 1859 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1796 - $this->script_add_vippslocale();
1860 + $this->script_add_vippslocale('vipps-gw');
1797 1861
1798 1862 wp_enqueue_script('vipps-gw');
1799 1863 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1800 1864 wp_enqueue_script('vipps-button-webcomponent');
@@ -1799,13 +1863,55 @@
1799 1863 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1800 1864 wp_enqueue_script('vipps-button-webcomponent');
1801 1865 }
1802 1866
1867 + // These scripts should be loaded only on the checkout screen and is used only for the classic shortcode checkout and
1868 + // the pay-for-order screen. IOK 2026-09-15
1869 + public function enqueue_classic_checkout_scripts () {
1870 + if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) {
1871 + return;
1872 + }
1873 + // Order-pay is rendered by the classic form even with a Blocks checkout page.
1874 + // It must bypass the check for the parent checkout page's block content.
1875 + if ( ! is_checkout_pay_page() ) {
1876 + $utils = '\\Automattic\\WooCommerce\\Blocks\\Utils\\CartCheckoutUtils';
1877 + $uses_checkout_block = is_callable( array( $utils, 'is_checkout_block_default' ) )
1878 + ? $utils::is_checkout_block_default()
1879 + : has_block( 'woocommerce/checkout', wc_get_page_id( 'checkout' ) );
1803 1880
1881 + if ( $uses_checkout_block ) {
1882 + return;
1883 + }
1884 + }
1885 +
1886 + // This script uses jQuery because the classic checkout screen does too. IOK 2026-09-15
1887 + $relative_path = 'js/vipps-classic-checkout.js';
1888 + wp_enqueue_script(
1889 + 'vipps-classic-checkout',
1890 + plugins_url( $relative_path, __FILE__ ),
1891 + array( 'jquery', 'wc-checkout', 'vipps-gw' ),
1892 + filemtime( plugin_dir_path( __FILE__ ) . $relative_path ),
1893 + true
1894 + );
1895 +
1896 + if ( is_checkout_pay_page() ) {
1897 + $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
1898 + wp_add_inline_script( 'vipps-classic-checkout', 'window.VippsOrderPayConfig = ' . wp_json_encode( array(
1899 + 'orderId' => $order ? $order->get_id() : 0,
1900 + 'orderKey' => $order ? $order->get_order_key() : '',
1901 + 'billingEmail' => $order ? $order->get_billing_email() : '',
1902 + 'endpoint' => $order ? rest_url( 'wc/store/v1/checkout/' . $order->get_id() ) : '',
1903 + 'nonce' => wp_create_nonce( 'wc_store_api' ),
1904 + 'billingAddress' => $order ? $order->get_address( 'billing' ) : array(),
1905 + 'shippingAddress' => $order ? $order->get_address( 'shipping' ) : array(),
1906 + ) ) . ';', 'before' );
1907 + }
1908 + }
1909 +
1910 +
1804 1911 public function add_shortcodes() {
1805 1912 add_shortcode('woo_vipps_buy_now', array($this, 'buy_now_button_shortcode'));
1806 1913 add_shortcode('woo_vipps_express_checkout_button', array($this, 'express_checkout_button_shortcode'));
1807 - add_shortcode('woo_vipps_express_checkout_banner', array($this, 'express_checkout_banner_shortcode'));
1808 1914
1809 1915 // Badges, if using shortcodes
1810 1916 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1811 1917 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
@@ -1840,8 +1946,10 @@
1840 1946 }
1841 1947
1842 1948 // Show express button option on checkout form. LP 2026-03-23
1843 1949 public function checkout_before_customer_details_express () {
1950 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1951 + do_action('woo_vipps_checkout_before_customer_details_express');
1844 1952 $gw = $this->gateway();
1845 1953 if (!$gw->show_express_checkout()) return;
1846 1954 $this->express_checkout_section_html();
1847 1955 }
@@ -1855,77 +1963,51 @@
1855 1963 $this->checkout_express_checkout_button_html();
1856 1964 echo '</fieldset>';
1857 1965 }
1858 1966
1859 - public function express_checkout_banner() {
1967 + // Show the express button if reasonable to do so
1968 + public function cart_express_checkout_button() {
1860 1969 $gw = $this->gateway();
1861 - if (!$gw->show_express_checkout()) return;
1862 - return $this->express_checkout_banner_html();
1863 - }
1864 1970
1865 - public function express_checkout_banner_html() {
1866 - $url = $this->express_checkout_url();
1867 - $url = wp_nonce_url($url,'express','sec');
1868 - $text = __('Skip entering your address and just checkout using', 'woo-vipps');
1869 - $linktext = 'Express'; // dont translate. LP 2025-09-03
1870 - $logo = $this->get_express_banner_logo();
1871 - $payment_method = $this->get_payment_method_name();
1872 -
1873 - $img_classes = 'express-banner-logo inline negative ' . strtolower($payment_method) . '-logo';
1874 - $div_classes = 'woocommerce-info ' . strtolower($payment_method) . '-info';
1875 - $a_classes = 'express-banner-link ' . strtolower($payment_method) . '-link';
1876 -
1877 - $message = $text . "<a href='$url' class='$a_classes'><img class='$img_classes' border=0 src='$logo' alt='$payment_method'/>$linktext!</a>";
1878 - $message = apply_filters('woo_vipps_express_checkout_banner', $message, $url, $payment_method);
1879 - ?>
1880 - <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1881 - <?php
1971 + if ($gw->show_express_checkout()){
1972 + return $this->cart_express_checkout_button_html();
1973 + }
1882 1974 }
1883 1975
1884 - public function checkout_express_checkout_button() {
1976 + public function minicart_express_checkout_button() {
1885 1977 $gw = $this->gateway();
1886 1978
1887 1979 if ($gw->show_express_checkout()){
1888 - return $this->checkout_express_checkout_button_html();
1980 + return $this->cart_express_checkout_button_html('minicart');
1889 1981 }
1890 1982 }
1891 1983
1892 - public function checkout_express_checkout_button_html() {
1893 - $url = $this->express_checkout_url();
1894 - $url = wp_nonce_url($url,'express','sec');
1895 - $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1984 + // This is for the Vipps SDK button used instead of the normal "pay for order" and "confirm order" buttons
1985 + // on the classic checkout and pay-for-order pages. It gets swapped in when the user selects vipps, and swapped out otherwise.
1986 + public function add_checkout_button_for_classic () {
1987 + $button = $this->get_html_button_for_context('checkout');
1988 + $submit = "<div class='vipps-classic-checkout-container'><button id='vipps-classic-checkout-submit' class='hidden vipps-submit-wrapper' type='submit'>$button</button></div>";
1989 + echo $submit;
1990 + }
1991 +
1992 + public function cart_express_checkout_button_html($context= 'cart') {
1993 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1896 1994 $method = $this->get_payment_method_name();
1897 1995 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1898 - $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
1996 + $url = "#";
1997 + $sec = wp_create_nonce('express');
1998 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
1899 1999 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1900 2000 echo $html;
1901 2001 }
1902 2002
1903 - // Show the express button if reasonable to do so
1904 - public function cart_express_checkout_button() {
1905 - $gw = $this->gateway();
1906 -
1907 - if ($gw->show_express_checkout()){
1908 - return $this->cart_express_checkout_button_html();
1909 - }
1910 - }
1911 -
1912 - public function minicart_express_checkout_button() {
1913 - $gw = $this->gateway();
1914 -
1915 - if ($gw->show_express_checkout()){
1916 - return $this->cart_express_checkout_button_html(true);
1917 - }
1918 - }
1919 -
1920 - public function cart_express_checkout_button_html($minicart = false) {
1921 - $url = $this->express_checkout_url();
1922 - $url = wp_nonce_url($url,'express','sec');
1923 - $context = $minicart ? 'minicart' : 'cart';
1924 - $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
2003 + public function checkout_express_checkout_button_html() {
2004 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1925 2005 $method = $this->get_payment_method_name();
1926 2006 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1927 - $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
2007 + $url = "#";
2008 + $sec = wp_create_nonce('express');
2009 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
1928 2010 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1929 2011 echo $html;
1930 2012 }
1931 2013
@@ -1964,19 +2046,11 @@
1964 2046 public function express_checkout_button_shortcode() {
1965 2047 $gw = $this->gateway();
1966 2048 if (!$gw->cart_supports_express_checkout()) return;
1967 2049 ob_start();
1968 - $this->cart_express_checkout_button_html('shortcode');
2050 + $this->cart_express_checkout_button_html('cart');
1969 2051 return ob_get_clean();
1970 2052 }
1971 - // 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
1972 - public function express_checkout_banner_shortcode() {
1973 - $gw = $this->gateway();
1974 - if (!$gw->cart_supports_express_checkout()) return;
1975 - ob_start();
1976 - $this->express_checkout_banner_html();
1977 - return ob_get_clean();
1978 - }
1979 2053
1980 2054 // Manage the various product meta fields
1981 2055 public function process_product_meta ($id, $post) {
1982 2056 // This is for the 'buy now' button
@@ -2647,12 +2721,36 @@
2647 2721 remove_filter('template_redirect', 'redirect_canonical', 10);
2648 2722 // dont cache special page. LP 2026-08-25
2649 2723 $this->nocache();
2650 2724 // Do the custom pre-load actions for these pages IOK 2026-09-11
2651 - do_action('woo_vipps_before_handling_special_page', $_GET['action']);
2725 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2652 2726 }
2653 2727 }
2654 2728
2729 + // Ran in template redirect for the special page. IOK 2026-09-2
2730 + public function pre_special_page_actions ($action) {
2731 + // Change title dynamically depending on action. LP 2026-09-02
2732 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2733 +
2734 + // If we are handling the 'wait for payment' action, we need to poll the order status before
2735 + // we start producing content IOK 2026-09-21
2736 + if ($action == 'wait_for_payment') {
2737 + $this->handle_payment_poll_and_redirect();
2738 + }
2739 +
2740 + // Some validation is required for this action
2741 + if ($action == 'do_express_checkout') {
2742 + $this->vipps_express_checkout_consistency_check();
2743 + }
2744 + // These two actions require an extra script
2745 + if (in_array($action, ['buy_product','do_express_checkout'])) {
2746 + wp_enqueue_script('vipps-purchase', plugins_url('js/vipps-purchase.js',__FILE__), ['vipps-gw'],
2747 + filemtime(dirname(__FILE__) . "/js/vipps-purchase.js"),
2748 + ['in_footer'=>true]
2749 + );
2750 + }
2751 + }
2752 +
2655 2753 // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2656 2754 public function vipps_special_page_endpoint_title($title, $postid = 0) {
2657 2755 global $wp_query;
2658 2756 // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
@@ -2795,14 +2893,18 @@
2795 2893 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2796 2894 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2797 2895
2798 2896 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2799 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2897 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2800 2898 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2801 2899
2802 2900 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2803 2901 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2804 2902
2903 + // For the classic checkout page and pay-for-order page, use a custom submit button when payment method
2904 + // is Vipps
2905 + add_action('woocommerce_review_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2906 + add_action('woocommerce_pay_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2805 2907
2806 2908 // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2807 2909 add_action('template_redirect', array($this,'template_redirect'),1);
2808 2910
@@ -2812,21 +2914,8 @@
2812 2914 // Ajax endpoints for checking the order status while waiting for confirmation
2813 2915 add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status'));
2814 2916 add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status'));
2815 2917
2816 -
2817 - // Buying a single product directly using express checkout IOK 2018-09-28
2818 - add_action('wp_ajax_nopriv_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2819 - add_action('wp_ajax_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2820 -
2821 - // This is for express checkout which we will also do asynchronously IOK 2018-05-28
2822 - add_action('wp_ajax_nopriv_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2823 - add_action('wp_ajax_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2824 -
2825 - // Same thing, but for single products IOK 2018-05-28
2826 - add_action('wp_ajax_nopriv_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2827 - add_action('wp_ajax_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2828 -
2829 2918 // Handle the cancel unpaid order action when the "hold stock" times out.
2830 2919 // For *normal* vipps orders, we run another cronjob every 5. minute which checks order status,
2831 2920 // therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return.
2832 2921 // For Checkout the rules are different though.
@@ -2905,9 +2994,8 @@
2905 2994 $this->vippsJSConfig = array();
2906 2995 $this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php');
2907 2996 $this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps');
2908 2997 $this->vippsJSConfig['BuyNowWithVipps'] = sprintf(__('Buy now with %1$s', 'woo-vipps'), $this->get_payment_method_name());
2909 - $this->vippsJSConfig['vippslogourl'] = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
2910 2998 $this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__);
2911 2999 $this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name());
2912 3000 $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());
2913 3001 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
@@ -2912,8 +3000,10 @@
2912 3000 $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());
2913 3001 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2914 3002 $this->vippsJSConfig['vippslocale'] = get_locale();
2915 3003 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
3004 + $this->vippsJSConfig['paymentMethodSlug'] = sanitize_title($this->get_payment_method_name());
3005 + $this->vippsJSConfig['paymentMethodName'] = $this->get_payment_method_name();
2916 3006
2917 3007
2918 3008 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2919 3009 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
@@ -4131,9 +4221,9 @@
4131 4221 WC()->cart->calculate_totals();
4132 4222 WC()->cart->set_session();
4133 4223 return true;
4134 4224 } catch (Exception $e) {
4135 - $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error');
4225 + $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->getMessage()), 'error');
4136 4226 return false;
4137 4227 }
4138 4228 }
4139 4229
@@ -4243,9 +4333,11 @@
4243 4333 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
4244 4334 return $url;
4245 4335 }
4246 4336 $url = $this->express_checkout_url();
4247 - $url = wp_nonce_url($url,'express','sec');
4337 + // At this point, there is always a query argument here. IOK 2026-09-21
4338 + $nonce = wp_create_nonce('express');
4339 + $url = $url . "&sec=$nonce";
4248 4340
4249 4341 return $url;
4250 4342 }
4251 4343
@@ -4442,28 +4534,12 @@
4442 4534 $order = wc_get_order($order->get_id());
4443 4535 $order_status = $order->get_status();
4444 4536
4445 4537 if ($order_status != 'pending') return $order_status;
4446 - // No callback has occured yet. If this has been going on for a while, check directly with Vipps
4447 - // We can't use the vipps init timestamp here, because that may be in the past for Checkout at least. IOK 2025-08-13
4448 - if ($order_status == 'pending') {
4449 - if (WC()->session) {
4450 - $now = time();
4451 - $then = WC()->session->get('_vipps_check_' . $order->get_id());
4452 - if (!$then) {
4453 - $then = $now;
4454 - WC()->session->set('_vipps_check_' . $order->get_id(), $then);
4455 - }
4456 - if (($then + (1 * 30)) > $now) { // more than half a minute? Start checking at Vipps
4457 - return $order_status;
4458 - }
4459 - } else {
4460 - // No session shouldn't be possible, but if it is..
4461 - return $order_status;
4462 - }
4463 - }
4538 +
4539 + $gw = $this->gateway();
4464 4540 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4465 - return $this->check_status_of_pending_order($order);
4541 + $newstatus = $gw->poll_and_check_order_status($order);
4466 4542 }
4467 4543
4468 4544 // In some situations we have to empty the cart when the user goes to Vipps, so
4469 4545 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
@@ -4531,8 +4607,9 @@
4531 4607
4532 4608 // Maybe log in user
4533 4609 // It is done on the thank-you page of the order, and only for express checkout.
4534 4610 function maybe_log_in_user ($order) {
4611 +
4535 4612 if (is_user_logged_in()) return;
4536 4613 if (!$order || ! self::is_vipps_order($order)) return;
4537 4614
4538 4615 // We *do* want to log in express checkout customers, but not those that
@@ -4678,129 +4755,233 @@
4678 4755 }
4679 4756 if (!$o) return;
4680 4757 if (!$o->get_meta('_vipps_single_product_express')) return;
4681 4758 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4682 - if ($failed) WC()->cart->empty_cart();
4759 + // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4760 + WC()->cart->empty_cart();
4683 4761 $this->restore_cart($o);
4684 4762 }
4685 4763
4686 4764
4687 - public function ajax_vipps_buy_single_product () {
4688 - Vipps::nocache();
4689 - static::set_locale_if_in_header();
4690 - // We're not checking ajax referer here, because what we do is creating a session and redirecting to the
4691 - // 'create order' page wherein we'll do the actual work. IOK 2018-09-28
4692 - $session = WC()->session;
4693 - if (!$session->has_session()) {
4694 - $session->set_customer_session_cookie(true);
4765 + // Actually create a express checkout order object, with no shipping or personal information, returning information about
4766 + // the result. The order should at this point be in a/the cart. For single product purchases, this is a different cart than
4767 + // the main one; for cart purchases, it's just the WC()->cart object. IOK 2026-08-25
4768 + private function create_and_process_express_order() {
4769 + $result = null;
4770 + $gw = $this->gateway();
4771 + try {
4772 + $orderid = $gw->create_partial_order();
4773 + do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4774 + } catch (Exception $e) {
4775 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4776 + return $result;
4777 + }
4778 + if (!$orderid) {
4779 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4780 + return $result;
4695 4781 }
4696 - $session->set('__vipps_buy_product', json_encode($_REQUEST));
4697 4782
4698 - // Incredibly, some caches will cache this page even with cookies set and no-cache headers set. So we try to
4699 - // add yet another way to inform caches that this is, in fact, not cacheable. IOK 2023-06-12
4700 - $url = add_query_arg('nc', sha1(uniqid(WC()->session->get_customer_id(),true)), $this->buy_product_url());
4783 + try {
4784 + $this->maybe_add_static_shipping($gw,$orderid);
4785 + } catch (Exception $e) {
4786 + $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4787 + $this->log($e->getMessage(),'error');
4788 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4789 + return $result;
4790 + }
4701 4791
4702 - $result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url);
4703 - wp_send_json($result);
4704 - exit();
4792 + // Now pass this to the Woo gateway and get a redirect URL back IOK 2026-08-25
4793 + $ok = $gw->process_payment($orderid);
4794 + if ($ok && $ok['result'] == 'success') {
4795 + $result = array('ok'=>1, 'orderid'=>$orderid, 'msg'=>'', 'url'=>$ok['redirect']);
4796 + return $result;
4797 + }
4798 + $result = array('ok'=>0, 'orderid'=>$orderid, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4799 + return $result;
4705 4800 }
4706 4801
4707 - public function ajax_do_express_checkout () {
4708 - check_ajax_referer('do_express','sec');
4709 - Vipps::nocache();
4710 - static::set_locale_if_in_header();
4802 + // 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
4803 + // avoid/warn the user of duplicate purchases. IOK 2026-09-09
4804 + public function create_order_hash($args=null) {
4805 + // If we have no arguments, we'll hash the cart.
4806 + if (empty($args)) {
4807 + $cartitems = WC()->cart->get_cart();
4808 + $orderspec = array();
4809 + foreach($cartitems as $item => $values) {
4810 + $orderspec[] = array('sku'=> ($values['sku'] ?? ""), 'product_id'=>($values['product_id'] ?? 0), 'variation_id'=>($values['variation_id'] ?? 0), 'quantity'=>($values['quantity'] ?? 1));
4811 + }
4812 + $args = $orderspec;
4813 + }
4814 + return md5(serialize($args));
4815 + }
4816 +
4817 +
4818 + // This method may provide HTML form elements to ask a user questions after starting
4819 + // express checkout. It is used to detect duplicate orders, possibly for terms and conditions, and user-definiable customizations. IOK 2026-09-09
4820 + // NULL productinfo means use the cart; the "current hash" is used to detect duplicates, and is calculated by the caller.
4821 + public function express_order_needs_confirmation($args, $productinfo, $current_hash) {
4822 + $elements = [];
4823 + $html = "";
4824 +
4825 + // First, let's check if we need to confirm the purchase.
4826 + $last_express_purchase_hash = WC()->session->get('woo_vipps_last_express');
4827 + if ($last_express_purchase_hash) {
4828 + list($hash, $orderid, $stamp) = explode(":", $last_express_purchase_hash);
4829 + $cutoff = $stamp + apply_filters('woo_vipps_recent_order_cutoff', (3*60));
4830 + if ($hash == $current_hash && (time() <= $cutoff )) {
4831 + $order = wc_get_order($orderid);
4832 + $status = $order ? $order->get_status() : false;
4833 + // 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
4834 + // a different flow. IOK 2026-09-17
4835 + if (in_array($status, ['on-hold', 'processing', 'completed'])) {
4836 + $header = __("Are you sure?",'woo-vipps');
4837 + $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');
4838 + $elements['possible_duplicate'] = "<h1>$header</h1><p>$body</p>";
4839 + $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');
4840 + }
4841 + }
4842 + }
4843 +
4711 4844 $gw = $this->gateway();
4845 + $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
4846 + $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
4847 + $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
4712 4848
4713 - if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4714 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4715 - wp_send_json($result);
4716 - exit();
4849 + if ($askForTerms) {
4850 + $termsHTML = '';
4851 + // Include shop terms
4852 + ob_start();
4853 + wc_get_template('checkout/terms.php');
4854 + $termsHTML = ob_get_clean();
4855 + $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
4856 + $elements['terms'] = $termsHTML;
4717 4857 }
4718 4858
4859 + // Custom fields
4860 + ob_start();
4861 + do_action('woo_vipps_express_checkout_orderspec_form', $productinfo, $args);
4862 + $extra_fields = ob_get_clean();
4863 + if (!empty($extra_fields)) {
4864 + $elements['extra'] = $extra_fields;
4865 + }
4719 4866
4720 -
4867 + if (!empty($elements)) {
4868 + $html = join("\n", array_values($elements));
4869 + $msg = join(",", array_keys($elements));
4870 + return ['ok'=>2, 'msg'=>$msg, 'html'=>$html, 'url'=>''];
4871 + }
4721 4872
4873 + return false;
4874 +
4875 + }
4876 +
4877 + public function rest_do_express_checkout ($request) {
4878 + Vipps::nocache();
4879 + check_ajax_referer('express', 'sec');
4880 + static::set_locale_if_in_header();
4881 + $args = $request->get_json_params();
4882 + if (!$args) {
4883 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4884 + }
4885 +
4886 + // Since this is the REST api, we need to load the cart manually here. IOK 2026-08-27
4887 + if ( is_null( WC()->cart ) ) {
4888 + WC()->frontend_includes();
4889 + if ( ! WC()->session instanceof WC_Session ) {
4890 + WC()->session = new WC_Session_Handler();
4891 + WC()->session->init();
4892 + }
4893 + if (is_null( WC()->customer)) {
4894 + WC()->customer = new WC_Customer( get_current_user_id(), true );
4895 + }
4896 + WC()->cart = new WC_Cart();
4897 + WC()->cart->get_cart_from_session();
4898 + }
4899 +
4900 +
4901 + $gw = $this->gateway();
4902 + if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4903 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4904 + return $result;
4905 + }
4722 4906 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4723 4907 $toolate = false;
4724 4908 $msg = "";
4725 4909 $valid = WC()->cart->check_cart_item_validity();
4726 4910 if ( is_wp_error( $valid) ) {
4727 - $toolate = true;
4728 - $msg = "<br>" . $valid->get_error_message();
4911 + $toolate = true;
4912 + $msg = "<br>" . $valid->get_error_message();
4729 4913 }
4730 4914 $stock = WC()->cart->check_cart_item_stock();
4731 - if ( is_wp_error( $stock) ) {
4732 - $toolate = true;
4733 - $msg = "<br>" . $stock->get_error_message();
4734 - }
4915 + if ( is_wp_error( $stock) ) {
4916 + $toolate = true;
4917 + $msg = "<br>" . $stock->get_error_message();
4918 + }
4735 4919
4736 4920 if ($toolate) {
4737 4921 $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);
4738 - wp_send_json($result);
4739 - exit();
4922 + return $result;
4740 4923 }
4741 4924
4742 - try {
4743 - $orderid = $gw->create_partial_order();
4744 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4745 - } catch (Exception $e) {
4746 - $this->log($e->getMessage(),'error');
4747 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4748 - wp_send_json($result);
4749 - exit();
4750 - }
4751 - if (!$orderid) {
4752 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4753 - wp_send_json($result);
4754 - exit();
4925 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4926 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4927 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4928 + $cookies = $args['cookies'] ?? [];
4929 + foreach($cookies as $key => $value) {
4930 + if (!isset($_COOKIE[$key])) {
4931 + $_COOKIE[$key] = $value;
4932 + }
4755 4933 }
4934 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4935 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4936 + $others =$args['post'] ?? [];
4937 + foreach($args['post'] as $key=>$value) {
4938 + $_POST[$key] = $value;
4939 + }
4756 4940
4757 - try {
4758 - $this->maybe_add_static_shipping($gw,$orderid);
4759 - } catch (Exception $e) {
4760 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4761 - $this->log($e->getMessage(),'error');
4762 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4763 - wp_send_json($result);
4764 - exit();
4941 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4942 + $current_hash = $this->create_order_hash();
4943 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4944 + if (!$confirmation) {
4945 + $result = $this->express_order_needs_confirmation($args, null, $current_hash);
4946 + if (!empty($result)) {
4947 + return $result;
4948 + }
4765 4949 }
4766 -
4767 - $ok = $gw->process_payment($orderid);
4768 - if ($ok && $ok['result'] == 'success') {
4769 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4770 - wp_send_json($result);
4771 - exit();
4950 +
4951 + $result = $this->create_and_process_express_order();
4952 + if ($result['ok'] == 1) {
4953 + $orderid = $result['orderid'];
4954 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4955 + WC()->session->save_data();
4772 4956 }
4773 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4774 - wp_send_json($result);
4775 - exit();
4957 + return $result;
4958 +
4776 4959 }
4777 4960
4778 - // 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
4779 - public function ajax_do_single_product_express_checkout() {
4780 - check_ajax_referer('do_express','sec');
4781 - Vipps::nocache();
4961 +
4962 + // Rest handler for single product express checkout. Expects arguments as JSON. IOK 2026-08-25
4963 + public function rest_do_single_product_express_checkout ($request) {
4964 + Vipps::nocache();
4782 4965 static::set_locale_if_in_header();
4783 - require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4784 - $gw = $this->gateway();
4785 -
4786 - if (!$gw->express_checkout_available()) {
4787 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4788 - wp_send_json($result);
4789 - exit();
4966 + $args = $request->get_json_params();
4967 + if (!$args) {
4968 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4790 4969 }
4970 + $result = ['ok' => 0, 'msg'=>'', 'orderid'=>0, 'url'=>''];
4791 4971
4972 + // We receive the varid, prodid, sku and quantity directly. One of these. The sku is the dominant one. IOK 2026-08-27
4973 + $varid = intval($args['variation_id'] ?? 0);
4974 + $prodid = intval($args['product_id'] ?? 0);
4975 + $sku = sanitize_text_field($args['sku'] ?? "");
4976 + $quantity = max(1, intval($args['quantity'] ?? 0));
4792 4977
4793 - // 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
4794 - // 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
4795 - $varid = intval(@$_POST['variation_id']);
4796 - $prodid = intval(@$_POST['product_id']);
4797 - $sku = sanitize_text_field(@$_POST['sku']);
4798 - $quant = intval(@$_POST['quantity']);
4799 4978
4800 - // Get any attributes posted for variable products (where one of the dimensions is "any" for instance)
4801 - $variations = array();
4802 - foreach ($_POST as $key => $value ) {
4979 + // We expect the variations - that is, the fields named "attribute_..." to be sent as post fields.
4980 + // We just need to sanitize them.
4981 + $variations = [];
4982 + $invars = $args['post'] ?? [];
4983 + foreach ($invars as $key => $value) {
4803 4984 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4804 4985 continue;
4805 4986 }
4806 4987 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
@@ -4805,15 +4986,94 @@
4805 4986 }
4806 4987 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4807 4988 }
4808 4989
4809 - $product = null;
4810 - $variant = null;
4811 - $parent = null;
4812 - $parentid = null;
4813 - $quantity = 1;
4814 - if ($quant && $quant>1) $quantity=$quant;
4990 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4991 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4992 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4993 + $cookies = $args['cookies'] ?? [];
4994 + foreach($cookies as $key => $value) {
4995 + if (!isset($_COOKIE[$key])) {
4996 + $_COOKIE[$key] = $value;
4997 + }
4998 + }
4815 4999
5000 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
5001 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
5002 + $others =$args['post'] ?? [];
5003 + foreach($args['post'] as $key=>$value) {
5004 + $_POST[$key] = $value;
5005 + }
5006 +
5007 + // 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
5008 + if ( is_null( WC()->cart ) ) {
5009 + WC()->frontend_includes();
5010 + if ( ! WC()->session instanceof WC_Session ) {
5011 + WC()->session = new WC_Session_Handler();
5012 + WC()->session->init();
5013 +
5014 + // If we don't have a session cookie, we need to set it, and also initialize the $_COOKIE value. IOK 2026-09-29
5015 + if (! WC()->session->get_session_cookie()) {
5016 + $store_session_cookie = function ( $options, $name, $value ) { $_COOKIE[$name] = $value; return $options;};
5017 + add_filter('woocommerce_set_cookie_options', $store_session_cookie, 10, 3);
5018 + try {
5019 + WC()->session->set_customer_session_cookie( true ); // We have to explicitly set the cookie if this session is fresh. IOK 2026-09-29
5020 + } finally {
5021 + remove_filter('woocommerce_set_cookie_options', $store_session_cookie, 10);
5022 + }
5023 + }
5024 + }
5025 + if (is_null( WC()->customer)) {
5026 + WC()->customer = new WC_Customer( get_current_user_id(), true );
5027 + }
5028 + WC()->cart = new WC_Cart();
5029 + WC()->cart->get_cart_from_session();
5030 + }
5031 +
5032 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
5033 + // We calculate this here so we can add it to the session later. IOK 2026-09-09
5034 + $orderspec = array('sku'=> $sku, 'product_id'=>$prodid, 'variation_id'=>$varid, 'quantity'=>$quantity);
5035 + $current_hash = $this->create_order_hash($orderspec);
5036 +
5037 + // Now to handle "extra questions" for an order, including terms + conditions and "possible duplicate order" IOK 2026-09-09
5038 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
5039 + if (!$confirmation) {
5040 + $result = $this->express_order_needs_confirmation($args, $orderspec, $current_hash);
5041 + if (!empty($result)) {
5042 + $response = new WP_REST_Response($result);
5043 + $response->set_status(200);
5044 + return $response;
5045 + }
5046 + }
5047 +
5048 + // Basically always return 200 after this, and always return an object with an 'ok' and a 'msg' value, possibly 'orderid' and 'url'.
5049 + $result = $this->really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity, $variations);
5050 + // 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.
5051 + // We'll reset it on order failure I think. IOK 2026-08-20 FIXME
5052 + if ($result['ok'] == 1) {
5053 + $orderid = $result['orderid'];
5054 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
5055 + WC()->session->save_data();
5056 + }
5057 +
5058 + $response = new WP_REST_Response($result);
5059 + $response->set_status(200);
5060 +
5061 + return $response;
5062 + }
5063 +
5064 + // Common private method to do single product express checkout, used by the new REST express. IOK 2026-08-25
5065 + private function really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity=1, $variations=[]) {
5066 + require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
5067 + $gw = $this->gateway();
5068 +
5069 + if (!$gw->express_checkout_available()) {
5070 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
5071 + return $result;
5072 + }
5073 + // 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
5074 + // 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
5075 +
4816 5076 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
4817 5077 // 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
4818 5078 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
4819 5079 try {
@@ -4826,17 +5086,14 @@
4826 5086 $product = wc_get_product($skuid);
4827 5087 }
4828 5088 } catch (Exception $e) {
4829 5089 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
4830 - wp_send_json($result);
4831 - exit();
5090 + return $result;
4832 5091 }
4833 5092
4834 -
4835 5093 if (!$product) {
4836 5094 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
4837 - wp_send_json($result);
4838 - exit();
5095 + return $result;
4839 5096 }
4840 5097
4841 5098 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
4842 5099 $parent = $parentid ? wc_get_product($parentid) : null;
@@ -4843,34 +5100,30 @@
4843 5100
4844 5101 // This can't really happen, but if it did..
4845 5102 if ($prodid && $parentid && ($prodid != $parentid)) {
4846 5103 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
4847 - wp_send_json($result);
4848 - exit();
5104 + return $result;
4849 5105 }
4850 5106 if (!$gw->product_supports_express_checkout($product)) {
4851 5107 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4852 - wp_send_json($result);
4853 - exit();
5108 + return $result;
4854 5109 }
4855 5110
4856 5111 // Somebody addded the wrong SKU
4857 5112 if ($product->get_type() == 'variable'){
4858 5113 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
4859 - wp_send_json($result);
4860 - exit();
5114 + return $result;
4861 5115 }
4862 5116 // Final check of availability
4863 5117 if (!$product->is_purchasable() || !$product->is_in_stock()) {
4864 5118 $result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false);
4865 - wp_send_json($result);
4866 - exit();
5119 + return $result;
4867 5120 }
4868 5121
4869 5122 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
4870 -
4871 5123 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
4872 5124 // because some plugins actually override this.
5125 + // NB: Please note the cart must have been loaded here, be aware when doing REST. IOK 2026-08-27
4873 5126 $current_cart = clone WC()->cart;
4874 5127 WC()->cart->empty_cart();
4875 5128
4876 5129 if ($parent && $parent->get_type() == 'variable') {
@@ -4877,50 +5130,22 @@
4877 5130 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
4878 5131 } else {
4879 5132 WC()->cart->add_to_cart($product->get_id(),$quantity);
4880 5133 }
5134 + WC()->session->save_data();
4881 5135
4882 - try {
4883 - $orderid = $gw->create_partial_order();
4884 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4885 - } catch (Exception $e) {
4886 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4887 - wp_send_json($result);
4888 - exit();
4889 - }
5136 + $result = $this->create_and_process_express_order();
4890 5137
4891 - if (!$orderid) {
4892 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4893 - wp_send_json($result);
4894 - exit();
5138 + if ($result['ok'] ?? false) {
5139 + // Single product purchase, so save any contents of the real cart
5140 + $orderid = $result['orderid'];
5141 + $order = wc_get_order($orderid);
5142 + $order->update_meta_data('_vipps_single_product_express',true);
5143 + $order->save();
5144 + $this->save_cart($order,$current_cart);
4895 5145 }
4896 5146
4897 - try {
4898 - $this->maybe_add_static_shipping($gw,$orderid);
4899 - } catch (Exception $e) {
4900 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4901 - $this->log($e->getMessage(),'error');
4902 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4903 - wp_send_json($result);
4904 - exit();
4905 - }
4906 -
4907 -
4908 - // Single product purchase, so save any contents of the real cart
4909 - $order = wc_get_order($orderid);
4910 - $order->update_meta_data('_vipps_single_product_express',true);
4911 - $order->save();
4912 - $this->save_cart($order,$current_cart);
4913 -
4914 - $ok = $gw->process_payment($orderid);
4915 - if ($ok && $ok['result'] == 'success') {
4916 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4917 - wp_send_json($result);
4918 - exit();
4919 - }
4920 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4921 - wp_send_json($result);
4922 - exit();
5147 + return $result;
4923 5148 }
4924 5149
4925 5150 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
4926 5151 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
@@ -4986,9 +5211,9 @@
4986 5211 $transaction = sanitize_text_field(@$_POST['transaction']);
4987 5212
4988 5213 $sessionorders= WC()->session->get('_vipps_session_orders');
4989 5214 if (!isset($sessionorders[$orderid])) {
4990 - wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps')));
5215 + wp_send_json(array('status'=>'error', 'msg'=>__('Not a session order','woo-vipps')));
4991 5216 }
4992 5217
4993 5218 $order = wc_get_order($orderid);
4994 5219 if (!$order) {
@@ -5245,9 +5470,8 @@
5245 5470 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5246 5471 }
5247 5472
5248 5473
5249 -
5250 5474 // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5251 5475 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5252 5476 // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5253 5477 public function woocommerce_create_pages ($data) {
@@ -5299,10 +5523,12 @@
5299 5523 // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5300 5524 if (is_admin()) return;
5301 5525 if (wp_doing_ajax()) return;
5302 5526 if (defined('REST_REQUEST') && REST_REQUEST) return;
5527 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5303 5528
5304 5529 $action = $_GET['action'] ?? '';
5530 + $html = "";
5305 5531 switch ($action) {
5306 5532 case 'wait_for_payment':
5307 5533 $html = $this->vipps_wait_for_payment();
5308 5534 break;
@@ -5314,8 +5540,10 @@
5314 5540 break;
5315 5541 default:
5316 5542 $html = '';
5317 5543 }
5544 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5545 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5318 5546
5319 5547 // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5320 5548 return $html;
5321 5549 }
@@ -5324,14 +5552,8 @@
5324 5552 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5325 5553 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5326 5554 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5327 5555 public function vipps_buy_product() {
5328 - add_filter('body_class', function ($classes) {
5329 - $classes[] = 'vipps-express-checkout';
5330 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5331 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5332 - });
5333 -
5334 5556 do_action('woo_vipps_express_checkout_page');
5335 5557
5336 5558 $session = WC()->session;
5337 5559 $posted = $session->get('__vipps_buy_product');
@@ -5365,32 +5587,33 @@
5365 5587 }
5366 5588
5367 5589 // Pass the productinfo to the express checkout form
5368 5590 $args = array();
5369 - $args['quantity'] = 1;
5370 - if (array_key_exists('product_id',$productinfo)) $args['product_id'] = intval($productinfo['product_id']);
5371 - if (array_key_exists('variation_id',$productinfo)) $args['variation_id'] = intval($productinfo['variation_id']);
5372 - if (array_key_exists('product_sku',$productinfo)) $args['sku'] = sanitize_text_field($productinfo['product_sku']);
5373 - if (array_key_exists('quantity',$productinfo)) $args['quantity'] = intval($productinfo['quantity']);
5591 + $args['product_id'] = esc_attr(intval($productinfo['product_id'] ?? 0));
5592 + $args['variation_id'] = esc_attr(intval($productinfo['variation_id'] ?? 0));
5593 + $args['sku'] = esc_attr(sanitize_text_field($productinfo['product_sku'] ?? ""));
5594 + $args['quantity'] = esc_attr(max(1, intval($productinfo['quantity'] ?? 0)));
5374 5595
5375 - // 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.
5376 - foreach ($productinfo as $key => $value) {
5377 - if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
5378 - continue;
5379 - }
5380 - $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5381 - }
5596 + $payment_method = $this->get_payment_method_name();
5597 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5598 + $bclass = esc_attr($payment_method);
5382 5599
5383 - return $this->express_checkout_page_html(true,'do_single_product_express_checkout',$args);
5600 + $content = "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5601 + $content .= "<div class='vipps-qr-purchase' style='visibility:hidden'>";
5602 + $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}';
5603 + >";
5604 + $content .= $this->get_html_button_for_context('global');
5605 + $content .= "</a>";
5606 + $content .= "</div>";
5607 +
5608 + return $content;
5384 5609 }
5385 5610
5386 - // 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.
5387 - public function vipps_express_checkout() {
5611 + public function vipps_express_checkout_consistency_check() {
5388 5612 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5389 5613 // IOK 2018-05-28
5390 5614 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5391 5615
5392 -
5393 5616 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5394 5617 if (!$backurl) $backurl = home_url();
5395 5618
5396 5619 if (!$ok) {
@@ -5404,215 +5627,39 @@
5404 5627 wp_redirect($backurl);
5405 5628 exit();
5406 5629 }
5407 5630
5408 - add_filter('body_class', function ($classes) {
5409 - $classes[] = 'vipps-express-checkout';
5410 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5411 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5412 - });
5413 -
5414 - do_action('woo_vipps_express_checkout_page');
5415 -
5416 - return $this->express_checkout_page_html(true, 'do_express_checkout');
5631 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5417 5632 }
5418 5633
5419 - // This method tries to ensure that a customer does not 'lose' the return page and
5420 - // starts ordering the same products twice. IOK 2020-01-22
5421 - protected function validate_express_checkout_orderspec ($orderspec) {
5422 - if (empty($orderspec)) return true; // It's not a duplicate, it's nothing.
5423 -
5424 - // First build for the current order an array of hash-tables keyed by prodid, varid and quantity.
5425 - $orderset = array();
5426 - foreach($orderspec as $entry) $orderset[] = join(':', $entry);
5427 -
5428 - // Then get open orders
5429 - $sessionorders = array();
5430 - $sessionorderdata = WC()->session->get('_vipps_session_orders');
5431 - if ($sessionorderdata) {
5432 - foreach(array_keys($sessionorderdata) as $oid) {
5433 - $orderobject = wc_get_order($oid);
5434 - // Check to see that this hasn't been deleted yet IOK 2020-01-07
5435 - if ($orderobject instanceof WC_Order) {
5436 - $sessionorders[] = $orderobject;
5437 - }
5438 - }
5634 + // 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.
5635 + // IOK 2026-09-09 - nowadays this is only used for compatibility mode. It will automatically start express checkout of the current cart when reached.
5636 + public function vipps_express_checkout() {
5637 + // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5638 + if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5639 + $content = __('Link expired, please try again', 'woo-vipps');
5640 + return $content;
5439 5641 }
5440 - // Nothing more to do here
5441 - if (empty($sessionorders)) return true;
5642 +
5643 + do_action('woo_vipps_express_checkout_page');
5442 5644
5443 - // And create a similar hash table for each of the open orders
5444 - $openorderdata = array();
5445 - foreach ($sessionorders as $open_order) {
5446 - $status = $open_order->get_status();
5447 - if ($status == 'cancelled' || $status == 'pending') continue;
5448 - $when = strtotime($open_order->get_date_modified());
5449 - $cutoff = $when + apply_filters('woo_vipps_recent_order_cutoff', (5*60));
5450 - if (time() > $cutoff) {
5451 - continue;
5452 - }
5453 - $orderdata = array();
5454 - foreach($open_order->get_items() as $item) {
5455 - $productspec = $item->get_product_id() . ':' . $item->get_variation_id() . ':' . $item->get_quantity();
5456 - $orderdata[] = $productspec;
5457 - }
5458 - $openorderdata[]=$orderdata;
5459 - }
5645 + $payment_method = $this->get_payment_method_name();
5646 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5647 + $bclass = esc_attr($payment_method);
5648 + $sec = esc_attr($_REQUEST['sec']);
5649 + $content = "";
5650 + $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5651 + $content .= '<div class="vipps-cart-purchase" style="visibility:hidden">"';
5652 + $content .= "<a href='javascript:void(0)' class='vipps-express-checkout short $bclass' data-vipps-autostart='true' data-sec='$sec' title='$btitle'>";
5653 + $content .= $this->get_html_button_for_context('global');
5654 + $content .="</a>";
5655 + $content .="</div>";
5460 5656
5461 - // 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.
5462 - foreach($openorderdata as $prevorder) {
5463 - $a = array_diff($prevorder, $orderset);
5464 - $b = array_diff($orderset, $prevorder);
5465 - if (empty($a) && empty($b)) {
5466 - $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');
5467 - return false;
5468 - }
5469 - }
5470 - // Else, order is good.
5471 - return true;
5657 + return $content;
5472 5658 }
5473 5659
5474 - // Returns a triple of productid, variantid and quantity from an array of arguments which can pass either these or a SKU value.
5475 - // Return value is like in a cart.
5476 - // Used to create an order in express checkout, and to see that this order isn't a repeat. IOK 2020-01-22
5477 - protected function get_orderspec_from_arguments ($productinfo) {
5478 - if (!$productinfo) return array();
5479 - $variantid = 0;
5480 - $productid = 0;
5481 - $quantity = intval(@$productinfo['quantity']);
5482 - if (!$quantity) $quantity = 1;
5483 - if (isset($productinfo['sku']) && $productinfo['sku']) {
5484 - $sku = $productinfo['sku'];
5485 - $skuid = wc_get_product_id_by_sku($sku);
5486 - $product = wc_get_product($skuid);
5487 - $parentid = $product ? $product->get_parent_id() : null;
5488 - if ($product) {
5489 - if ($parentid) {
5490 - $variantid = $skuid; $productid = $parentid;
5491 - } else {
5492 - $productid = $skuid;
5493 - }
5494 - }
5495 - } else if (isset($productinfo['product_id']) && $productinfo['product_id']) {
5496 - $productid = intval($productinfo['product_id']);
5497 - $variantid = intval(@$productinfo['variation_id']);
5498 - }
5499 - if ($productid) return array(array('product_id'=>$productid, 'variation_id'=>$variantid, 'quantity'=>$quantity));
5500 - return array();
5501 - }
5502 - // If no productinfo, this will produce an orderspec from the current cart IOK 2020-01-24
5503 - protected function get_orderspec_from_cart () {
5504 - $cartitems = WC()->cart->get_cart();
5505 - $orderspec = array();
5506 - foreach($cartitems as $item => $values) {
5507 - $orderspec[] = array('product_id'=>$values['product_id'], 'variation_id'=>$values['variation_id'], 'quantity'=>$values['quantity']);
5508 - }
5509 - return $orderspec;
5510 - }
5511 -
5512 - // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5513 - // Returns the html. LP 2026-08-27
5514 - protected function express_checkout_page_html($execute,$action,$productinfo=null) {
5515 - $gw = $this->gateway();
5516 -
5517 - $expressCheckoutMessages = array();
5518 - $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
5519 - $expressCheckoutMessages['temporaryError'] = sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name());
5520 - $expressCheckoutMessages['successMessage'] = sprintf(__('To the %1$s app!','woo-vipps'), $this->get_payment_method_name());
5521 -
5522 - 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');
5523 - wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5524 - wp_enqueue_script('vipps-express-checkout');
5525 - // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5526 - // to actually perform the express checkout.
5527 - $buttonhtml = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button());
5528 -
5529 -
5530 -
5531 - $orderspec = $this->get_orderspec_from_arguments($productinfo);
5532 - if (empty($orderspec)) {
5533 - $orderspec = $this->get_orderspec_from_cart();
5534 - }
5535 - $orderisOK = $this->validate_express_checkout_orderspec($orderspec);
5536 - $orderisOK = apply_filters('woo_vipps_validate_express_checkout_orderspec', $orderisOK, $orderspec);
5537 -
5538 - $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
5539 - $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
5540 - $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
5541 -
5542 - $askForConfirmationHTML = '';
5543 - if (!$orderisOK) {
5544 - $header = __("Are you sure?",'woo-vipps');
5545 - $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');
5546 - $askForConfirmationHTML = apply_filters('woo_vipps_ask_user_to_confirm_repurchase', "<h2 class='confirmVippsExpressCheckoutHeader'>$header</h2><p>$body</p>");
5547 - }
5548 - // Should we go directly to checkout, or do we need to stop and ask the user something (for instance?) IOK 2010-01-20
5549 - $execute = $execute && $orderisOK && !$askForTerms;
5550 - $execute = apply_filters('woo_vipps_checkout_directly_to_vipps', $execute, $productinfo);
5551 -
5552 - $content = $this->spinner();
5553 -
5554 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5555 - // The form data below is sent on order creation; the sec is also used to poll session status
5556 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5557 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5558 - $content .= "<input type='hidden' name='action' value='" . esc_attr($action) ."'>";
5559 - if ($this->gateway()->get_option('vippsorderattribution') == 'yes') {
5560 - // This is for the new order attribution feature of woo. IOK 2024-01-09
5561 - $content .= '<input type="hidden" id="vippsorderattribution" value="1" />';
5562 - ob_start();
5563 - do_action( 'woocommerce_after_order_notes');
5564 - $content .= ob_get_clean();
5565 - }
5566 - $content .= wp_nonce_field('do_express','sec',1,false);
5567 -
5568 - $termsHTML = '';
5569 - if ($askForTerms) {
5570 - // Include shop terms
5571 - ob_start();
5572 - wc_get_template('checkout/terms.php');
5573 - $termsHTML = ob_get_clean();
5574 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5575 - }
5576 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5577 -
5578 - if ($productinfo) {
5579 - foreach($productinfo as $key=>$value) {
5580 - $k = esc_attr($key);
5581 - $v = esc_attr($value);
5582 - $content .= "<input type='hidden' name='$k' value='$v' />";
5583 - }
5584 - }
5585 - ob_start();
5586 - $content .= do_action('woo_vipps_express_checkout_orderspec_form', $productinfo);
5587 - $content .= ob_get_clean();
5588 - $content .= "</form>";
5589 -
5590 - $extraHTML = apply_filters('woo_vipps_express_checkout_final_html', '', $termsHTML,$askForConfirmationHTML);
5591 - $pressTheButtonHTML = "";
5592 - if (empty($termsHTML) && empty($askForConfirmationHTML) && empty($extraHTML)) {
5593 - $pressTheButtonHTML = "<p id=waiting>" . sprintf(__('Ready for %1$s - press the button', 'woo-vipps'), Vipps::ExpressCheckoutName()) . "</p>";
5594 - }
5595 -
5596 - if ($execute) {
5597 - $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5598 - $content .= "<div id='vipps-status-message'></div>";
5599 - return $this->special_page_html('', $content);
5600 - } else {
5601 - $content .= $askForConfirmationHTML;
5602 - $content .= $extraHTML;
5603 - $content .= $termsHTML;
5604 - $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5605 - $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5606 - $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>";
5607 - $content .= "<div id='vipps-status-message'></div>";
5608 - return $this->special_page_html('', $content);
5609 - }
5610 - }
5611 -
5612 -
5613 -
5614 - public function vipps_wait_for_payment() {
5660 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5661 + private function handle_payment_poll_and_redirect () {
5615 5662 $orderid = WC()->session->get('_vipps_pending_order');
5616 5663
5617 5664 $order = null;
5618 5665 $gw = $this->gateway();
@@ -5626,9 +5673,9 @@
5626 5673 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5627 5674 // simulating the session with that.
5628 5675 // IOK 2019-11-19, changed to using GET 2023-01-23
5629 5676 if ($no_session && $limited_session) {
5630 - $orderid = intval(@$_GET['id']);
5677 + $orderid = intval($_GET['id'] ?? false);
5631 5678 }
5632 5679 if ($orderid) {
5633 5680 clean_post_cache($orderid);
5634 5681 $order = wc_get_order($orderid);
@@ -5643,19 +5690,22 @@
5643 5690 $session = WC()->session;
5644 5691 if (!$session->has_session()) {
5645 5692 $session->set_customer_session_cookie(true);
5646 5693 }
5694 +
5695 + $sessionorders= WC()->session->get('_vipps_session_orders');
5696 + $sessionorders[$orderid] = 1;
5697 + WC()->session->set('_vipps_session_orders',$sessionorders);
5647 5698 $session->set('_vipps_pending_order', $orderid);
5699 + WC()->session->save_data();
5648 5700 }
5649 5701 }
5650 5702
5651 - do_action('woo_vipps_wait_for_payment_page',$order);
5652 -
5653 5703 $deleted_order=0;
5654 5704 if ($orderid && !$order) {
5655 5705 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5656 5706 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
5657 - $this->log(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), 'debug');
5707 + $this->log(sprintf(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), $orderid), 'debug');
5658 5708 $deleted_order=1;
5659 5709 }
5660 5710
5661 5711 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
@@ -5665,12 +5715,13 @@
5665 5715
5666 5716 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5667 5717 $do_poll = true;
5668 5718
5669 - // Still pending, no callback. Make a call to the server as the order might not have been created. IOK 2018-05-16
5719 + // Do a single poll here to check and set the order status at Woo using the order status at Vipps IOK 2026-09-29
5670 5720 if ($do_poll && $status == 'pending') {
5671 - // Just in case the callback hasn't come yet, do a quick check of the order status at Vipps.
5672 - $newstatus = $gw->callback_check_order_status($order);
5721 + // We will do *one* poll before waiting for the callback (for a while, at least.) IOK 2026-09-29
5722 + $newstatus = $gw->poll_and_check_order_status($order);
5723 + $this->log(sprintf(__("In order return: Order status of %1\$d is %2\$s", 'woo-vipps'), $orderid, $newstatus), 'info');
5673 5724 if ($status != $newstatus) {
5674 5725 $status = $newstatus;
5675 5726 clean_post_cache($orderid);
5676 5727 $order = wc_get_order($orderid); // Reload order object
@@ -5675,11 +5726,13 @@
5675 5726 clean_post_cache($orderid);
5676 5727 $order = wc_get_order($orderid); // Reload order object
5677 5728 }
5678 5729 } else {
5679 - // No need to do anyting here. IOK 2020-01-26
5730 + // No need to do anyting here. IOK 2020-01-26
5680 5731 }
5681 5732
5733 + // Actually, this may cause a second poll if the first left us pending. Should be rewritten - but *mostly* it will just check
5734 + // 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
5682 5735 $payment = 'notchecked';
5683 5736 if ($do_poll) {
5684 5737 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5685 5738 }
@@ -5693,9 +5746,8 @@
5693 5746 exit();
5694 5747 }
5695 5748
5696 5749 // We are done, but in failure. Don't poll.
5697 - $content = "";
5698 5750 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5699 5751
5700 5752 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5701 5753 // 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
@@ -5703,8 +5755,9 @@
5703 5755 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5704 5756 wp_redirect($failure_redirect);
5705 5757 exit();
5706 5758 }
5759 +
5707 5760 if ($status == 'cancelled' || $payment == 'cancelled') {
5708 5761 $this->maybe_restore_cart($orderid,'failed');
5709 5762 if ($failure_redirect){
5710 5763 wp_redirect($failure_redirect);
@@ -5709,8 +5762,38 @@
5709 5762 if ($failure_redirect){
5710 5763 wp_redirect($failure_redirect);
5711 5764 exit();
5712 5765 }
5766 + } else {
5767 + // If not, enqueue the status checker IOK 2026-09-21
5768 + 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');
5769 + }
5770 +
5771 + $this->log(sprintf(__("Order status of %1\$d not ready in order return: payment status %2\$s", 'woo-vipps'), $orderid, $payment), 'info');
5772 +
5773 + // Communicate this to the shortcode IOK 2026-09-21
5774 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5775 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5776 + });
5777 +
5778 + }
5779 +
5780 + public function vipps_wait_for_payment() {
5781 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5782 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5783 +
5784 + $orderid = $data['orderid'] ?? 0;
5785 + $status = $data['status'] ?? "";
5786 + $payment = $data['payment'] ?? "";
5787 +
5788 + $order = wc_get_order($orderid);
5789 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5790 +
5791 + do_action('woo_vipps_wait_for_payment_page',$order);
5792 + $gw = $this->gateway();
5793 +
5794 + $content = "";
5795 + if ($status == 'cancelled' || $payment == 'cancelled') {
5713 5796 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5714 5797 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5715 5798 $content .= "</div>";
5716 5799 return $this->special_page_html('', $content);
@@ -5716,12 +5799,9 @@
5716 5799 return $this->special_page_html('', $content);
5717 5800 }
5718 5801
5719 5802 // 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
5720 -
5721 5803 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5722 - 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');
5723 -
5724 5804 $signal = $this->callbackSignal($order);
5725 5805 $content = "";
5726 5806 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5727 5807
@@ -5729,17 +5809,17 @@
5729 5809 $signalurl = $this->callbackSignalURL($signal);
5730 5810
5731 5811 $content .= "</p></div>";
5732 5812
5733 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5734 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5735 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5813 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5814 +
5815 + // Carry the order status to the checking script IOK 2026-09-21
5816 + $content .= "<form id='vippsdata'>";
5736 5817 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5737 5818 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5738 5819 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5739 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5820 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5740 5821 $content .= "</form>";
5741 -
5742 5822
5743 5823 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5744 5824 $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>";
5745 5825 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';