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 +585 -512 6.2.1 → 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'));
@@ -1857,77 +1963,51 @@
1857 1963 $this->checkout_express_checkout_button_html();
1858 1964 echo '</fieldset>';
1859 1965 }
1860 1966
1861 - public function express_checkout_banner() {
1967 + // Show the express button if reasonable to do so
1968 + public function cart_express_checkout_button() {
1862 1969 $gw = $this->gateway();
1863 - if (!$gw->show_express_checkout()) return;
1864 - return $this->express_checkout_banner_html();
1865 - }
1866 1970
1867 - public function express_checkout_banner_html() {
1868 - $url = $this->express_checkout_url();
1869 - $url = wp_nonce_url($url,'express','sec');
1870 - $text = __('Skip entering your address and just checkout using', 'woo-vipps');
1871 - $linktext = 'Express'; // dont translate. LP 2025-09-03
1872 - $logo = $this->get_express_banner_logo();
1873 - $payment_method = $this->get_payment_method_name();
1874 -
1875 - $img_classes = 'express-banner-logo inline negative ' . strtolower($payment_method) . '-logo';
1876 - $div_classes = 'woocommerce-info ' . strtolower($payment_method) . '-info';
1877 - $a_classes = 'express-banner-link ' . strtolower($payment_method) . '-link';
1878 -
1879 - $message = $text . "<a href='$url' class='$a_classes'><img class='$img_classes' border=0 src='$logo' alt='$payment_method'/>$linktext!</a>";
1880 - $message = apply_filters('woo_vipps_express_checkout_banner', $message, $url, $payment_method);
1881 - ?>
1882 - <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1883 - <?php
1971 + if ($gw->show_express_checkout()){
1972 + return $this->cart_express_checkout_button_html();
1973 + }
1884 1974 }
1885 1975
1886 - public function checkout_express_checkout_button() {
1976 + public function minicart_express_checkout_button() {
1887 1977 $gw = $this->gateway();
1888 1978
1889 1979 if ($gw->show_express_checkout()){
1890 - return $this->checkout_express_checkout_button_html();
1980 + return $this->cart_express_checkout_button_html('minicart');
1891 1981 }
1892 1982 }
1893 1983
1894 - public function checkout_express_checkout_button_html() {
1895 - $url = $this->express_checkout_url();
1896 - $url = wp_nonce_url($url,'express','sec');
1897 - $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));
1898 1994 $method = $this->get_payment_method_name();
1899 1995 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1900 - $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>";
1901 1999 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1902 2000 echo $html;
1903 2001 }
1904 2002
1905 - // Show the express button if reasonable to do so
1906 - public function cart_express_checkout_button() {
1907 - $gw = $this->gateway();
1908 -
1909 - if ($gw->show_express_checkout()){
1910 - return $this->cart_express_checkout_button_html();
1911 - }
1912 - }
1913 -
1914 - public function minicart_express_checkout_button() {
1915 - $gw = $this->gateway();
1916 -
1917 - if ($gw->show_express_checkout()){
1918 - return $this->cart_express_checkout_button_html(true);
1919 - }
1920 - }
1921 -
1922 - public function cart_express_checkout_button_html($minicart = false) {
1923 - $url = $this->express_checkout_url();
1924 - $url = wp_nonce_url($url,'express','sec');
1925 - $context = $minicart ? 'minicart' : 'cart';
1926 - $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'));
1927 2005 $method = $this->get_payment_method_name();
1928 2006 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1929 - $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>";
1930 2010 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1931 2011 echo $html;
1932 2012 }
1933 2013
@@ -1966,19 +2046,11 @@
1966 2046 public function express_checkout_button_shortcode() {
1967 2047 $gw = $this->gateway();
1968 2048 if (!$gw->cart_supports_express_checkout()) return;
1969 2049 ob_start();
1970 - $this->cart_express_checkout_button_html('shortcode');
2050 + $this->cart_express_checkout_button_html('cart');
1971 2051 return ob_get_clean();
1972 2052 }
1973 - // 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
1974 - public function express_checkout_banner_shortcode() {
1975 - $gw = $this->gateway();
1976 - if (!$gw->cart_supports_express_checkout()) return;
1977 - ob_start();
1978 - $this->express_checkout_banner_html();
1979 - return ob_get_clean();
1980 - }
1981 2053
1982 2054 // Manage the various product meta fields
1983 2055 public function process_product_meta ($id, $post) {
1984 2056 // This is for the 'buy now' button
@@ -2649,12 +2721,36 @@
2649 2721 remove_filter('template_redirect', 'redirect_canonical', 10);
2650 2722 // dont cache special page. LP 2026-08-25
2651 2723 $this->nocache();
2652 2724 // Do the custom pre-load actions for these pages IOK 2026-09-11
2653 - do_action('woo_vipps_before_handling_special_page', $_GET['action']);
2725 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2654 2726 }
2655 2727 }
2656 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 +
2657 2753 // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2658 2754 public function vipps_special_page_endpoint_title($title, $postid = 0) {
2659 2755 global $wp_query;
2660 2756 // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
@@ -2803,8 +2899,12 @@
2803 2899
2804 2900 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2805 2901 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2806 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'));
2807 2907
2808 2908 // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2809 2909 add_action('template_redirect', array($this,'template_redirect'),1);
2810 2910
@@ -2814,21 +2914,8 @@
2814 2914 // Ajax endpoints for checking the order status while waiting for confirmation
2815 2915 add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status'));
2816 2916 add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status'));
2817 2917
2818 -
2819 - // Buying a single product directly using express checkout IOK 2018-09-28
2820 - add_action('wp_ajax_nopriv_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2821 - add_action('wp_ajax_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2822 -
2823 - // This is for express checkout which we will also do asynchronously IOK 2018-05-28
2824 - add_action('wp_ajax_nopriv_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2825 - add_action('wp_ajax_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2826 -
2827 - // Same thing, but for single products IOK 2018-05-28
2828 - add_action('wp_ajax_nopriv_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2829 - add_action('wp_ajax_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2830 -
2831 2918 // Handle the cancel unpaid order action when the "hold stock" times out.
2832 2919 // For *normal* vipps orders, we run another cronjob every 5. minute which checks order status,
2833 2920 // therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return.
2834 2921 // For Checkout the rules are different though.
@@ -2907,9 +2994,8 @@
2907 2994 $this->vippsJSConfig = array();
2908 2995 $this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php');
2909 2996 $this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps');
2910 2997 $this->vippsJSConfig['BuyNowWithVipps'] = sprintf(__('Buy now with %1$s', 'woo-vipps'), $this->get_payment_method_name());
2911 - $this->vippsJSConfig['vippslogourl'] = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
2912 2998 $this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__);
2913 2999 $this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name());
2914 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());
2915 3001 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
@@ -2914,8 +3000,10 @@
2914 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());
2915 3001 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2916 3002 $this->vippsJSConfig['vippslocale'] = get_locale();
2917 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();
2918 3006
2919 3007
2920 3008 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2921 3009 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
@@ -4133,9 +4221,9 @@
4133 4221 WC()->cart->calculate_totals();
4134 4222 WC()->cart->set_session();
4135 4223 return true;
4136 4224 } catch (Exception $e) {
4137 - $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');
4138 4226 return false;
4139 4227 }
4140 4228 }
4141 4229
@@ -4245,9 +4333,11 @@
4245 4333 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
4246 4334 return $url;
4247 4335 }
4248 4336 $url = $this->express_checkout_url();
4249 - $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";
4250 4340
4251 4341 return $url;
4252 4342 }
4253 4343
@@ -4444,28 +4534,12 @@
4444 4534 $order = wc_get_order($order->get_id());
4445 4535 $order_status = $order->get_status();
4446 4536
4447 4537 if ($order_status != 'pending') return $order_status;
4448 - // No callback has occured yet. If this has been going on for a while, check directly with Vipps
4449 - // We can't use the vipps init timestamp here, because that may be in the past for Checkout at least. IOK 2025-08-13
4450 - if ($order_status == 'pending') {
4451 - if (WC()->session) {
4452 - $now = time();
4453 - $then = WC()->session->get('_vipps_check_' . $order->get_id());
4454 - if (!$then) {
4455 - $then = $now;
4456 - WC()->session->set('_vipps_check_' . $order->get_id(), $then);
4457 - }
4458 - if (($then + (1 * 30)) > $now) { // more than half a minute? Start checking at Vipps
4459 - return $order_status;
4460 - }
4461 - } else {
4462 - // No session shouldn't be possible, but if it is..
4463 - return $order_status;
4464 - }
4465 - }
4538 +
4539 + $gw = $this->gateway();
4466 4540 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4467 - return $this->check_status_of_pending_order($order);
4541 + $newstatus = $gw->poll_and_check_order_status($order);
4468 4542 }
4469 4543
4470 4544 // In some situations we have to empty the cart when the user goes to Vipps, so
4471 4545 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
@@ -4533,8 +4607,9 @@
4533 4607
4534 4608 // Maybe log in user
4535 4609 // It is done on the thank-you page of the order, and only for express checkout.
4536 4610 function maybe_log_in_user ($order) {
4611 +
4537 4612 if (is_user_logged_in()) return;
4538 4613 if (!$order || ! self::is_vipps_order($order)) return;
4539 4614
4540 4615 // We *do* want to log in express checkout customers, but not those that
@@ -4680,129 +4755,233 @@
4680 4755 }
4681 4756 if (!$o) return;
4682 4757 if (!$o->get_meta('_vipps_single_product_express')) return;
4683 4758 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4684 - 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();
4685 4761 $this->restore_cart($o);
4686 4762 }
4687 4763
4688 4764
4689 - public function ajax_vipps_buy_single_product () {
4690 - Vipps::nocache();
4691 - static::set_locale_if_in_header();
4692 - // We're not checking ajax referer here, because what we do is creating a session and redirecting to the
4693 - // 'create order' page wherein we'll do the actual work. IOK 2018-09-28
4694 - $session = WC()->session;
4695 - if (!$session->has_session()) {
4696 - $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;
4697 4781 }
4698 - $session->set('__vipps_buy_product', json_encode($_REQUEST));
4699 4782
4700 - // Incredibly, some caches will cache this page even with cookies set and no-cache headers set. So we try to
4701 - // add yet another way to inform caches that this is, in fact, not cacheable. IOK 2023-06-12
4702 - $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 + }
4703 4791
4704 - $result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url);
4705 - wp_send_json($result);
4706 - 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;
4707 4800 }
4708 4801
4709 - public function ajax_do_express_checkout () {
4710 - check_ajax_referer('do_express','sec');
4711 - Vipps::nocache();
4712 - 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 +
4713 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);
4714 4848
4715 - if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4716 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4717 - wp_send_json($result);
4718 - 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;
4719 4857 }
4720 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 + }
4721 4866
4722 -
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 + }
4723 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 + }
4724 4906 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4725 4907 $toolate = false;
4726 4908 $msg = "";
4727 4909 $valid = WC()->cart->check_cart_item_validity();
4728 4910 if ( is_wp_error( $valid) ) {
4729 - $toolate = true;
4730 - $msg = "<br>" . $valid->get_error_message();
4911 + $toolate = true;
4912 + $msg = "<br>" . $valid->get_error_message();
4731 4913 }
4732 4914 $stock = WC()->cart->check_cart_item_stock();
4733 - if ( is_wp_error( $stock) ) {
4734 - $toolate = true;
4735 - $msg = "<br>" . $stock->get_error_message();
4736 - }
4915 + if ( is_wp_error( $stock) ) {
4916 + $toolate = true;
4917 + $msg = "<br>" . $stock->get_error_message();
4918 + }
4737 4919
4738 4920 if ($toolate) {
4739 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);
4740 - wp_send_json($result);
4741 - exit();
4922 + return $result;
4742 4923 }
4743 4924
4744 - try {
4745 - $orderid = $gw->create_partial_order();
4746 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4747 - } catch (Exception $e) {
4748 - $this->log($e->getMessage(),'error');
4749 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4750 - wp_send_json($result);
4751 - exit();
4752 - }
4753 - if (!$orderid) {
4754 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4755 - wp_send_json($result);
4756 - 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 + }
4757 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 + }
4758 4940
4759 - try {
4760 - $this->maybe_add_static_shipping($gw,$orderid);
4761 - } catch (Exception $e) {
4762 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4763 - $this->log($e->getMessage(),'error');
4764 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4765 - wp_send_json($result);
4766 - 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 + }
4767 4949 }
4768 -
4769 - $ok = $gw->process_payment($orderid);
4770 - if ($ok && $ok['result'] == 'success') {
4771 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4772 - wp_send_json($result);
4773 - 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();
4774 4956 }
4775 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4776 - wp_send_json($result);
4777 - exit();
4957 + return $result;
4958 +
4778 4959 }
4779 4960
4780 - // 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
4781 - public function ajax_do_single_product_express_checkout() {
4782 - check_ajax_referer('do_express','sec');
4783 - 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();
4784 4965 static::set_locale_if_in_header();
4785 - require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4786 - $gw = $this->gateway();
4787 -
4788 - if (!$gw->express_checkout_available()) {
4789 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4790 - wp_send_json($result);
4791 - 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]);
4792 4969 }
4970 + $result = ['ok' => 0, 'msg'=>'', 'orderid'=>0, 'url'=>''];
4793 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));
4794 4977
4795 - // 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
4796 - // 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
4797 - $varid = intval(@$_POST['variation_id']);
4798 - $prodid = intval(@$_POST['product_id']);
4799 - $sku = sanitize_text_field(@$_POST['sku']);
4800 - $quant = intval(@$_POST['quantity']);
4801 4978
4802 - // Get any attributes posted for variable products (where one of the dimensions is "any" for instance)
4803 - $variations = array();
4804 - 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) {
4805 4984 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4806 4985 continue;
4807 4986 }
4808 4987 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
@@ -4807,15 +4986,94 @@
4807 4986 }
4808 4987 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4809 4988 }
4810 4989
4811 - $product = null;
4812 - $variant = null;
4813 - $parent = null;
4814 - $parentid = null;
4815 - $quantity = 1;
4816 - 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 + }
4817 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 +
4818 5076 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
4819 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
4820 5078 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
4821 5079 try {
@@ -4828,17 +5086,14 @@
4828 5086 $product = wc_get_product($skuid);
4829 5087 }
4830 5088 } catch (Exception $e) {
4831 5089 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
4832 - wp_send_json($result);
4833 - exit();
5090 + return $result;
4834 5091 }
4835 5092
4836 -
4837 5093 if (!$product) {
4838 5094 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
4839 - wp_send_json($result);
4840 - exit();
5095 + return $result;
4841 5096 }
4842 5097
4843 5098 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
4844 5099 $parent = $parentid ? wc_get_product($parentid) : null;
@@ -4845,34 +5100,30 @@
4845 5100
4846 5101 // This can't really happen, but if it did..
4847 5102 if ($prodid && $parentid && ($prodid != $parentid)) {
4848 5103 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
4849 - wp_send_json($result);
4850 - exit();
5104 + return $result;
4851 5105 }
4852 5106 if (!$gw->product_supports_express_checkout($product)) {
4853 5107 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4854 - wp_send_json($result);
4855 - exit();
5108 + return $result;
4856 5109 }
4857 5110
4858 5111 // Somebody addded the wrong SKU
4859 5112 if ($product->get_type() == 'variable'){
4860 5113 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
4861 - wp_send_json($result);
4862 - exit();
5114 + return $result;
4863 5115 }
4864 5116 // Final check of availability
4865 5117 if (!$product->is_purchasable() || !$product->is_in_stock()) {
4866 5118 $result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false);
4867 - wp_send_json($result);
4868 - exit();
5119 + return $result;
4869 5120 }
4870 5121
4871 5122 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
4872 -
4873 5123 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
4874 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
4875 5126 $current_cart = clone WC()->cart;
4876 5127 WC()->cart->empty_cart();
4877 5128
4878 5129 if ($parent && $parent->get_type() == 'variable') {
@@ -4879,50 +5130,22 @@
4879 5130 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
4880 5131 } else {
4881 5132 WC()->cart->add_to_cart($product->get_id(),$quantity);
4882 5133 }
5134 + WC()->session->save_data();
4883 5135
4884 - try {
4885 - $orderid = $gw->create_partial_order();
4886 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4887 - } catch (Exception $e) {
4888 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4889 - wp_send_json($result);
4890 - exit();
4891 - }
5136 + $result = $this->create_and_process_express_order();
4892 5137
4893 - if (!$orderid) {
4894 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4895 - wp_send_json($result);
4896 - 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);
4897 5145 }
4898 5146
4899 - try {
4900 - $this->maybe_add_static_shipping($gw,$orderid);
4901 - } catch (Exception $e) {
4902 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4903 - $this->log($e->getMessage(),'error');
4904 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4905 - wp_send_json($result);
4906 - exit();
4907 - }
4908 -
4909 -
4910 - // Single product purchase, so save any contents of the real cart
4911 - $order = wc_get_order($orderid);
4912 - $order->update_meta_data('_vipps_single_product_express',true);
4913 - $order->save();
4914 - $this->save_cart($order,$current_cart);
4915 -
4916 - $ok = $gw->process_payment($orderid);
4917 - if ($ok && $ok['result'] == 'success') {
4918 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4919 - wp_send_json($result);
4920 - exit();
4921 - }
4922 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4923 - wp_send_json($result);
4924 - exit();
5147 + return $result;
4925 5148 }
4926 5149
4927 5150 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
4928 5151 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
@@ -4988,9 +5211,9 @@
4988 5211 $transaction = sanitize_text_field(@$_POST['transaction']);
4989 5212
4990 5213 $sessionorders= WC()->session->get('_vipps_session_orders');
4991 5214 if (!isset($sessionorders[$orderid])) {
4992 - 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')));
4993 5216 }
4994 5217
4995 5218 $order = wc_get_order($orderid);
4996 5219 if (!$order) {
@@ -5247,9 +5470,8 @@
5247 5470 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5248 5471 }
5249 5472
5250 5473
5251 -
5252 5474 // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5253 5475 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5254 5476 // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5255 5477 public function woocommerce_create_pages ($data) {
@@ -5330,15 +5552,8 @@
5330 5552 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5331 5553 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5332 5554 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5333 5555 public function vipps_buy_product() {
5334 -
5335 - add_filter('body_class', function ($classes) {
5336 - $classes[] = 'vipps-express-checkout';
5337 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5338 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5339 - });
5340 -
5341 5556 do_action('woo_vipps_express_checkout_page');
5342 5557
5343 5558 $session = WC()->session;
5344 5559 $posted = $session->get('__vipps_buy_product');
@@ -5372,32 +5587,33 @@
5372 5587 }
5373 5588
5374 5589 // Pass the productinfo to the express checkout form
5375 5590 $args = array();
5376 - $args['quantity'] = 1;
5377 - if (array_key_exists('product_id',$productinfo)) $args['product_id'] = intval($productinfo['product_id']);
5378 - if (array_key_exists('variation_id',$productinfo)) $args['variation_id'] = intval($productinfo['variation_id']);
5379 - if (array_key_exists('product_sku',$productinfo)) $args['sku'] = sanitize_text_field($productinfo['product_sku']);
5380 - 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)));
5381 5595
5382 - // 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.
5383 - foreach ($productinfo as $key => $value) {
5384 - if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
5385 - continue;
5386 - }
5387 - $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5388 - }
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);
5389 5599
5390 - 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;
5391 5609 }
5392 5610
5393 - // 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.
5394 - public function vipps_express_checkout() {
5611 + public function vipps_express_checkout_consistency_check() {
5395 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.
5396 5613 // IOK 2018-05-28
5397 5614 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5398 5615
5399 -
5400 5616 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5401 5617 if (!$backurl) $backurl = home_url();
5402 5618
5403 5619 if (!$ok) {
@@ -5411,215 +5627,39 @@
5411 5627 wp_redirect($backurl);
5412 5628 exit();
5413 5629 }
5414 5630
5415 - add_filter('body_class', function ($classes) {
5416 - $classes[] = 'vipps-express-checkout';
5417 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5418 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5419 - });
5420 -
5421 - do_action('woo_vipps_express_checkout_page');
5422 -
5423 - return $this->express_checkout_page_html(true, 'do_express_checkout');
5631 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5424 5632 }
5425 5633
5426 - // This method tries to ensure that a customer does not 'lose' the return page and
5427 - // starts ordering the same products twice. IOK 2020-01-22
5428 - protected function validate_express_checkout_orderspec ($orderspec) {
5429 - if (empty($orderspec)) return true; // It's not a duplicate, it's nothing.
5430 -
5431 - // First build for the current order an array of hash-tables keyed by prodid, varid and quantity.
5432 - $orderset = array();
5433 - foreach($orderspec as $entry) $orderset[] = join(':', $entry);
5434 -
5435 - // Then get open orders
5436 - $sessionorders = array();
5437 - $sessionorderdata = WC()->session->get('_vipps_session_orders');
5438 - if ($sessionorderdata) {
5439 - foreach(array_keys($sessionorderdata) as $oid) {
5440 - $orderobject = wc_get_order($oid);
5441 - // Check to see that this hasn't been deleted yet IOK 2020-01-07
5442 - if ($orderobject instanceof WC_Order) {
5443 - $sessionorders[] = $orderobject;
5444 - }
5445 - }
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;
5446 5641 }
5447 - // Nothing more to do here
5448 - if (empty($sessionorders)) return true;
5642 +
5643 + do_action('woo_vipps_express_checkout_page');
5449 5644
5450 - // And create a similar hash table for each of the open orders
5451 - $openorderdata = array();
5452 - foreach ($sessionorders as $open_order) {
5453 - $status = $open_order->get_status();
5454 - if ($status == 'cancelled' || $status == 'pending') continue;
5455 - $when = strtotime($open_order->get_date_modified());
5456 - $cutoff = $when + apply_filters('woo_vipps_recent_order_cutoff', (5*60));
5457 - if (time() > $cutoff) {
5458 - continue;
5459 - }
5460 - $orderdata = array();
5461 - foreach($open_order->get_items() as $item) {
5462 - $productspec = $item->get_product_id() . ':' . $item->get_variation_id() . ':' . $item->get_quantity();
5463 - $orderdata[] = $productspec;
5464 - }
5465 - $openorderdata[]=$orderdata;
5466 - }
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>";
5467 5656
5468 - // 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.
5469 - foreach($openorderdata as $prevorder) {
5470 - $a = array_diff($prevorder, $orderset);
5471 - $b = array_diff($orderset, $prevorder);
5472 - if (empty($a) && empty($b)) {
5473 - $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');
5474 - return false;
5475 - }
5476 - }
5477 - // Else, order is good.
5478 - return true;
5657 + return $content;
5479 5658 }
5480 5659
5481 - // Returns a triple of productid, variantid and quantity from an array of arguments which can pass either these or a SKU value.
5482 - // Return value is like in a cart.
5483 - // Used to create an order in express checkout, and to see that this order isn't a repeat. IOK 2020-01-22
5484 - protected function get_orderspec_from_arguments ($productinfo) {
5485 - if (!$productinfo) return array();
5486 - $variantid = 0;
5487 - $productid = 0;
5488 - $quantity = intval(@$productinfo['quantity']);
5489 - if (!$quantity) $quantity = 1;
5490 - if (isset($productinfo['sku']) && $productinfo['sku']) {
5491 - $sku = $productinfo['sku'];
5492 - $skuid = wc_get_product_id_by_sku($sku);
5493 - $product = wc_get_product($skuid);
5494 - $parentid = $product ? $product->get_parent_id() : null;
5495 - if ($product) {
5496 - if ($parentid) {
5497 - $variantid = $skuid; $productid = $parentid;
5498 - } else {
5499 - $productid = $skuid;
5500 - }
5501 - }
5502 - } else if (isset($productinfo['product_id']) && $productinfo['product_id']) {
5503 - $productid = intval($productinfo['product_id']);
5504 - $variantid = intval(@$productinfo['variation_id']);
5505 - }
5506 - if ($productid) return array(array('product_id'=>$productid, 'variation_id'=>$variantid, 'quantity'=>$quantity));
5507 - return array();
5508 - }
5509 - // If no productinfo, this will produce an orderspec from the current cart IOK 2020-01-24
5510 - protected function get_orderspec_from_cart () {
5511 - $cartitems = WC()->cart->get_cart();
5512 - $orderspec = array();
5513 - foreach($cartitems as $item => $values) {
5514 - $orderspec[] = array('product_id'=>$values['product_id'], 'variation_id'=>$values['variation_id'], 'quantity'=>$values['quantity']);
5515 - }
5516 - return $orderspec;
5517 - }
5518 -
5519 - // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5520 - // Returns the html. LP 2026-08-27
5521 - protected function express_checkout_page_html($execute,$action,$productinfo=null) {
5522 - $gw = $this->gateway();
5523 -
5524 - $expressCheckoutMessages = array();
5525 - $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
5526 - $expressCheckoutMessages['temporaryError'] = sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name());
5527 - $expressCheckoutMessages['successMessage'] = sprintf(__('To the %1$s app!','woo-vipps'), $this->get_payment_method_name());
5528 -
5529 - 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');
5530 - wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5531 - wp_enqueue_script('vipps-express-checkout');
5532 - // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5533 - // to actually perform the express checkout.
5534 - $buttonhtml = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button());
5535 -
5536 -
5537 -
5538 - $orderspec = $this->get_orderspec_from_arguments($productinfo);
5539 - if (empty($orderspec)) {
5540 - $orderspec = $this->get_orderspec_from_cart();
5541 - }
5542 - $orderisOK = $this->validate_express_checkout_orderspec($orderspec);
5543 - $orderisOK = apply_filters('woo_vipps_validate_express_checkout_orderspec', $orderisOK, $orderspec);
5544 -
5545 - $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
5546 - $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
5547 - $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
5548 -
5549 - $askForConfirmationHTML = '';
5550 - if (!$orderisOK) {
5551 - $header = __("Are you sure?",'woo-vipps');
5552 - $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');
5553 - $askForConfirmationHTML = apply_filters('woo_vipps_ask_user_to_confirm_repurchase', "<h2 class='confirmVippsExpressCheckoutHeader'>$header</h2><p>$body</p>");
5554 - }
5555 - // Should we go directly to checkout, or do we need to stop and ask the user something (for instance?) IOK 2010-01-20
5556 - $execute = $execute && $orderisOK && !$askForTerms;
5557 - $execute = apply_filters('woo_vipps_checkout_directly_to_vipps', $execute, $productinfo);
5558 -
5559 - $content = $this->spinner();
5560 -
5561 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5562 - // The form data below is sent on order creation; the sec is also used to poll session status
5563 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5564 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5565 - $content .= "<input type='hidden' name='action' value='" . esc_attr($action) ."'>";
5566 - if ($this->gateway()->get_option('vippsorderattribution') == 'yes') {
5567 - // This is for the new order attribution feature of woo. IOK 2024-01-09
5568 - $content .= '<input type="hidden" id="vippsorderattribution" value="1" />';
5569 - ob_start();
5570 - do_action( 'woocommerce_after_order_notes');
5571 - $content .= ob_get_clean();
5572 - }
5573 - $content .= wp_nonce_field('do_express','sec',1,false);
5574 -
5575 - $termsHTML = '';
5576 - if ($askForTerms) {
5577 - // Include shop terms
5578 - ob_start();
5579 - wc_get_template('checkout/terms.php');
5580 - $termsHTML = ob_get_clean();
5581 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5582 - }
5583 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5584 -
5585 - if ($productinfo) {
5586 - foreach($productinfo as $key=>$value) {
5587 - $k = esc_attr($key);
5588 - $v = esc_attr($value);
5589 - $content .= "<input type='hidden' name='$k' value='$v' />";
5590 - }
5591 - }
5592 - ob_start();
5593 - $content .= do_action('woo_vipps_express_checkout_orderspec_form', $productinfo);
5594 - $content .= ob_get_clean();
5595 - $content .= "</form>";
5596 -
5597 - $extraHTML = apply_filters('woo_vipps_express_checkout_final_html', '', $termsHTML,$askForConfirmationHTML);
5598 - $pressTheButtonHTML = "";
5599 - if (empty($termsHTML) && empty($askForConfirmationHTML) && empty($extraHTML)) {
5600 - $pressTheButtonHTML = "<p id=waiting>" . sprintf(__('Ready for %1$s - press the button', 'woo-vipps'), Vipps::ExpressCheckoutName()) . "</p>";
5601 - }
5602 -
5603 - if ($execute) {
5604 - $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5605 - $content .= "<div id='vipps-status-message'></div>";
5606 - return $this->special_page_html('', $content);
5607 - } else {
5608 - $content .= $askForConfirmationHTML;
5609 - $content .= $extraHTML;
5610 - $content .= $termsHTML;
5611 - $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5612 - $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5613 - $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>";
5614 - $content .= "<div id='vipps-status-message'></div>";
5615 - return $this->special_page_html('', $content);
5616 - }
5617 - }
5618 -
5619 -
5620 -
5621 - 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 () {
5622 5662 $orderid = WC()->session->get('_vipps_pending_order');
5623 5663
5624 5664 $order = null;
5625 5665 $gw = $this->gateway();
@@ -5633,9 +5673,9 @@
5633 5673 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5634 5674 // simulating the session with that.
5635 5675 // IOK 2019-11-19, changed to using GET 2023-01-23
5636 5676 if ($no_session && $limited_session) {
5637 - $orderid = intval(@$_GET['id']);
5677 + $orderid = intval($_GET['id'] ?? false);
5638 5678 }
5639 5679 if ($orderid) {
5640 5680 clean_post_cache($orderid);
5641 5681 $order = wc_get_order($orderid);
@@ -5650,19 +5690,22 @@
5650 5690 $session = WC()->session;
5651 5691 if (!$session->has_session()) {
5652 5692 $session->set_customer_session_cookie(true);
5653 5693 }
5694 +
5695 + $sessionorders= WC()->session->get('_vipps_session_orders');
5696 + $sessionorders[$orderid] = 1;
5697 + WC()->session->set('_vipps_session_orders',$sessionorders);
5654 5698 $session->set('_vipps_pending_order', $orderid);
5699 + WC()->session->save_data();
5655 5700 }
5656 5701 }
5657 5702
5658 - do_action('woo_vipps_wait_for_payment_page',$order);
5659 -
5660 5703 $deleted_order=0;
5661 5704 if ($orderid && !$order) {
5662 5705 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5663 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
5664 - $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');
5665 5708 $deleted_order=1;
5666 5709 }
5667 5710
5668 5711 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
@@ -5672,12 +5715,13 @@
5672 5715
5673 5716 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5674 5717 $do_poll = true;
5675 5718
5676 - // 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
5677 5720 if ($do_poll && $status == 'pending') {
5678 - // Just in case the callback hasn't come yet, do a quick check of the order status at Vipps.
5679 - $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');
5680 5724 if ($status != $newstatus) {
5681 5725 $status = $newstatus;
5682 5726 clean_post_cache($orderid);
5683 5727 $order = wc_get_order($orderid); // Reload order object
@@ -5682,11 +5726,13 @@
5682 5726 clean_post_cache($orderid);
5683 5727 $order = wc_get_order($orderid); // Reload order object
5684 5728 }
5685 5729 } else {
5686 - // No need to do anyting here. IOK 2020-01-26
5730 + // No need to do anyting here. IOK 2020-01-26
5687 5731 }
5688 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
5689 5735 $payment = 'notchecked';
5690 5736 if ($do_poll) {
5691 5737 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5692 5738 }
@@ -5700,9 +5746,8 @@
5700 5746 exit();
5701 5747 }
5702 5748
5703 5749 // We are done, but in failure. Don't poll.
5704 - $content = "";
5705 5750 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5706 5751
5707 5752 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5708 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
@@ -5710,8 +5755,9 @@
5710 5755 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5711 5756 wp_redirect($failure_redirect);
5712 5757 exit();
5713 5758 }
5759 +
5714 5760 if ($status == 'cancelled' || $payment == 'cancelled') {
5715 5761 $this->maybe_restore_cart($orderid,'failed');
5716 5762 if ($failure_redirect){
5717 5763 wp_redirect($failure_redirect);
@@ -5716,8 +5762,38 @@
5716 5762 if ($failure_redirect){
5717 5763 wp_redirect($failure_redirect);
5718 5764 exit();
5719 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') {
5720 5796 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5721 5797 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5722 5798 $content .= "</div>";
5723 5799 return $this->special_page_html('', $content);
@@ -5723,12 +5799,9 @@
5723 5799 return $this->special_page_html('', $content);
5724 5800 }
5725 5801
5726 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
5727 -
5728 5803 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5729 - 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');
5730 -
5731 5804 $signal = $this->callbackSignal($order);
5732 5805 $content = "";
5733 5806 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5734 5807
@@ -5736,17 +5809,17 @@
5736 5809 $signalurl = $this->callbackSignalURL($signal);
5737 5810
5738 5811 $content .= "</p></div>";
5739 5812
5740 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5741 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5742 - $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'>";
5743 5817 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5744 5818 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5745 5819 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5746 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5820 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5747 5821 $content .= "</form>";
5748 -
5749 5822
5750 5823 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5751 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>";
5752 5825 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';