PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.3.1
Pay with Vipps and MobilePay for WooCommerce v6.3.1
6.3.1 6.2.6 6.3.0 6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 All 190 releases
← All changes | payment/Vipps.class.php +604 -600 6.2.0 → 6.3.1 View file →
@@ -54,11 +54,8 @@
54 54
55 55 // True if HPOS is being used
56 56 public $HPOSActive = null;
57 57
58 - // used in the fake locking mechanism using transients
59 - private $lockKey = null;
60 -
61 58 public $vippsJSConfig = array();
62 59
63 60 public $button_options_version = '2.0';
64 61 public $button_options_express_version = '2.0';
@@ -117,9 +114,10 @@
117 114 add_action('wp_footer', array($Vipps,'footer'));
118 115 }
119 116 add_action( 'plugins_loaded', array($Vipps,'plugins_loaded'));
120 117 add_action( 'after_setup_theme', array($Vipps,'after_setup_theme'));
121 - add_action('init',array($Vipps,'init'));
118 + add_action( 'init',array($Vipps,'init'));
119 + add_action( 'rest_api_init', array($Vipps, 'rest_api_init'));
122 120 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
123 121 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
124 122 add_action( 'woocommerce_blocks_loaded', [$Vipps, 'woocommerce_blocks_loaded']);
125 123 // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
@@ -227,8 +225,9 @@
227 225 // Register certain scripts in wp_loaded because they will be added to the backend as well - the gutenberg checkout block
228 226 // needs these to be defined in the backend. IOK 2024-04-16
229 227 add_action('wp_loaded', array($this, 'wp_register_scripts'));
230 228 add_action('wp_enqueue_scripts', array($this, 'wp_enqueue_scripts'));
229 + add_action('wp_enqueue_scripts', array($this, 'enqueue_classic_checkout_scripts'), 20);
231 230
232 231 // Remove the possibility of restarting failed orders etc. This will be fixed in the future. IOK 2023-05-26
233 232 add_filter('woocommerce_my_account_my_orders_actions', array($this,'woocommerce_my_account_my_orders_actions'), 10, 2);
234 233
@@ -244,16 +243,8 @@
244 243
245 244 // Extra order actions on the order screen, now using ajax to be compatible with HPOS. IOK 2022-12-02
246 245 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
247 246
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 247
257 248 // We need a 5-minute scheduled event for the handler for missed callbacks. Using the
258 249 // action scheduler would be better, but we can't do that just yet because of backwards
259 250 // compatibility. At some point, support for older woo-versions should be dropped; then this
@@ -276,15 +267,8 @@
276 267 // because it is self-updating or because it has been deactivated just now or something, we won't have access to it.
277 268 // Therefore test it first. IOK 2022-12-08
278 269 $gw = $this->gateway();
279 270
280 - // This is a developer-mode level feature because flock() is not portable. This ensures callbacks and shopreturns do not
281 - // simultaneously update the orders, in particular not the express checkout order lines wrt shipping. IOK 2020-05-19
282 - if ($gw && $gw->get_option('use_flock') == 'yes') {
283 - add_filter('woo_vipps_lock_order', array($this,'flock_lock_order'));
284 - add_action('woo_vipps_unlock_order', array($this, 'flock_unlock_order'));
285 - }
286 -
287 271 // Set default button options, migrating any older setup IOK 2026-07-15
288 272 $this->init_button_options();
289 273
290 274 /*
@@ -310,16 +294,36 @@
310 294
311 295
312 296 // We want this special page to have a certain title and maybe special scripts and so on,
313 297 // 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 - });
298 + add_action('woo_vipps_before_handling_special_page', array($this, 'pre_special_page_actions'));
318 299
319 300 // Add an admin interface for this page as well IOK 2026-09-11
320 301 add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
302 + }
321 303
304 +
305 + public function rest_api_init () {
306 +
307 + // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
308 + register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
309 + 'methods' => 'GET',
310 + 'callback' => [$this, 'rest_express_checkout_products'],
311 + 'permission_callback' => '__return_true',
312 + ]);
313 +
314 + // Start a single product express checkout process. IOK 2026-08-25
315 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout_single', [
316 + 'methods' => 'POST',
317 + 'callback' => [$this, 'rest_do_single_product_express_checkout'],
318 + 'permission_callback' => '__return_true',
319 + ]);
320 + // And one for the cart. IOK 2026-09-04
321 + register_rest_route(self::get_rest_namespace('v1'), '/express_checkout', [
322 + 'methods' => 'POST',
323 + 'callback' => [$this, 'rest_do_express_checkout'],
324 + 'permission_callback' => '__return_true',
325 + ]);
322 326 }
323 327
324 328 public function admin_init () {
325 329 $gw = $this->gateway();
@@ -348,10 +352,8 @@
348 352 // Styling etc
349 353 add_action('admin_head', array($this, 'admin_head'));
350 354
351 355 // Scripts
352 - $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
353 - wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
354 356 add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts'));
355 357
356 358 // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
357 359 add_action('current_screen', function ($screen) {
@@ -1046,12 +1048,30 @@
1046 1048
1047 1049 public function get_html_button_attrs_for_context($context = 'global') {
1048 1050 $options = get_option('vipps_button_options2', []);
1049 1051 if (!is_string($context)) $context = 'global';
1052 +
1053 + // Gutenberg express checkout buttons really want to be stretched, so we'll treat them somewhat differently.
1054 + $gutenberg = false;
1055 + if ($context == 'checkout_gutenberg') {
1056 + $context = 'checkout';
1057 + $gutenberg = true;
1058 + }
1059 + if ($context == 'cart_gutenberg') {
1060 + $context = 'cart';
1061 + $gutenberg = true;
1062 + }
1063 +
1050 1064 $config = $options['express']['configs'][$context] ?? [];
1051 - if (!$config || ($config['use-global-config'] ?? false)) {
1065 + $use_global = !$config || ($config['use-global-config'] ?? false);
1066 + if ($use_global) {
1052 1067 $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
1053 1068 }
1069 +
1070 + // see above.
1071 + if ($gutenberg) {
1072 + $config['stretched']='true';
1073 + }
1054 1074 return $config;
1055 1075 }
1056 1076
1057 1077 public function get_html_button_for_context($context = 'global') {
@@ -1071,8 +1091,10 @@
1071 1091 // Don't support these login verbs. LP 2026-06-04
1072 1092 if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy';
1073 1093 // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1074 1094 if ('dk' === $attrs['language']) $attrs['language'] = 'da';
1095 + // Fix swedish too. LP 2026-10-06
1096 + if ('se' === $attrs['language']) $attrs['language'] = 'sv';
1075 1097
1076 1098 $escaped_attrs = [];
1077 1099 foreach($attrs as $k => $v) {
1078 1100 $escaped_attrs[$k] = esc_attr($v);
@@ -1138,8 +1160,10 @@
1138 1160 private function button_menu_express_section() {
1139 1161 $options = get_option('vipps_button_options2', []);
1140 1162 $express = $options['express'] ?? [];
1141 1163 $configs = $express['configs'] ?? [];
1164 +
1165 +
1142 1166 $contexts = [
1143 1167 'global' => __('Global', 'woo-vipps'),
1144 1168 'product' => __('Product', 'woo-vipps'),
1145 1169 'catalog' => __('Catalog', 'woo-vipps'),
@@ -1312,8 +1336,9 @@
1312 1336
1313 1337 // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1314 1338 const newContext = jQuery("#context").val();
1315 1339 const newConfig = contextConfigs[newContext];
1340 +
1316 1341 setInputsFromConfig(newContext, newConfig);
1317 1342 currentContext = newContext;
1318 1343 }
1319 1344
@@ -1676,12 +1701,14 @@
1676 1701 <?php
1677 1702 }
1678 1703 // Scripts used in the backend
1679 1704 public function admin_enqueue_scripts($hook) {
1705 +
1706 + wp_register_script('vipps-admin',plugins_url('js/admin.js',__FILE__),array('jquery'),filemtime(dirname(__FILE__) . "/js/admin.js"), 'all');
1707 + $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
1708 + wp_localize_script('vipps-admin', 'VippsConfig', $this->vippsJSConfig);
1680 1709 // 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');
1710 + $this->script_add_vippslocale('vipps-admin');
1684 1711 wp_enqueue_script('vipps-admin');
1685 1712
1686 1713 wp_enqueue_style('vipps-admin-style',plugins_url('css/admin.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/admin.css"), 'all');
1687 1714 wp_enqueue_style('vipps-fonts');
@@ -1751,12 +1778,9 @@
1751 1778
1752 1779 public function wp_register_scripts () {
1753 1780 // We are going to use the 'hooks' library introduced by WP 5.1, but we still support WP 4.7. So if this isn't enqueues
1754 1781 // (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');
1782 + wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks', 'wp-api-fetch','vipps-widget-sdk'),filemtime(dirname(__FILE__) . "/js/vipps.js"), true);
1759 1783
1760 1784 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1761 1785 wp_register_script('vipps-onsite-messageing',
1762 1786 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
@@ -1762,13 +1786,27 @@
1762 1786 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1763 1787 array(),
1764 1788 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1765 1789 [
1766 - 'in_footer' => true,
1767 - 'strategy' => 'async',
1790 + 'in_footer' => true,
1791 + 'strategy' => 'async',
1768 1792 ],
1769 1793 );
1770 1794
1795 + add_filter( 'script_loader_tag', function($tag, $handle,$src) {
1796 + if ($handle == 'vipps-widget-sdk') {
1797 + $tag = preg_replace("!^<script!", "<script data-vipps-widget-sdk ", $tag);
1798 + return $tag;
1799 + }
1800 + return $tag;
1801 + },10,3);
1802 +
1803 + wp_register_script('vipps-widget-sdk', "https://cdn.vippsmobilepay.com/js/widget-sdk/vipps-widget.js",
1804 + array('vipps-button-webcomponent'),
1805 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps.js'),
1806 + ['in_footer' => true]
1807 + );
1808 +
1771 1809 // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1772 1810 wp_register_script('vipps-button-webcomponent',
1773 1811 plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1774 1812 array(),
@@ -1774,27 +1812,45 @@
1774 1812 array(),
1775 1813 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1776 1814 [
1777 1815 'in_footer' => false
1778 - ],
1816 + ]
1779 1817 );
1780 1818 }
1781 1819
1782 1820 // 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 () {
1821 + public function script_add_vippslocale ($handle) {
1784 1822 // This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13
1823 + $name = $this->get_payment_method_name();
1785 1824 $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()),
1825 + 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $name),
1826 + 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $name),
1827 + 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $name),
1828 + 'termsAndConditionsError' => __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ),
1829 + 'temporaryError' => sprintf(__('%1$s is temporarily unavailable.','woo-vipps'),$name),
1830 + 'successMessage' => sprintf(__('To the %1$s app!','woo-vipps'), $name),
1831 + 'cancel'=> __("Cancel", 'woo-vipps'),
1832 + 'close'=> __("Close", 'woo-vipps'),
1833 + 'missingPaymentUrl'=> __("Successful checkout response has no payment URL", 'woo-vipps'),
1834 + 'expressCheckoutFailed'=> __("Express checkout failed", 'woo-vipps'),
1835 + 'unexpectedCheckoutResponse'=> __("Unexpected express checkout response", 'woo-vipps'),
1836 + 'vippsCheckoutFailed'=> __("Vipps Mobilepay checkout failed", 'woo-vipps'),
1837 + 'correctHighlightedFields'=> __("Please correct the highlighted fields.", 'woo-vipps'),
1838 + 'checkFormBeforeContinuing'=> __("Please check the form before continuing.", 'woo-vipps'),
1839 + 'cartCheckoutUnavailable'=> __("Cannot start express checkout: cart checkout is unavailable", 'woo-vipps'),
1840 + 'productIdentifiersMissing'=> __("Cannot buy product: product id, variation id and sku are missing", 'woo-vipps'),
1841 + 'productFormNotFound'=> __("Cannot buy product: product form not found", 'woo-vipps'),
1842 + 'paymentSuccessfulRedirecting' => __("Payment successful. Redirecting…", 'woo-vipps'),
1789 1843 );
1790 - wp_localize_script('vipps-gw', 'VippsLocale', $strings);
1844 + wp_localize_script($handle, 'VippsLocale', $strings);
1791 1845 }
1792 1846
1793 1847 public function wp_enqueue_scripts() {
1848 + // Add late: if this value isn't 'yes' we wil not add order attribution to express orders. IOK 2026-09-10
1849 + $this->vippsJSConfig['expressOrderAttribution'] = $this->gateway()->get_option('vippsorderattribution');
1794 1850 wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
1795 1851 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1796 - $this->script_add_vippslocale();
1852 + $this->script_add_vippslocale('vipps-gw');
1797 1853
1798 1854 wp_enqueue_script('vipps-gw');
1799 1855 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1800 1856 wp_enqueue_script('vipps-button-webcomponent');
@@ -1799,13 +1855,55 @@
1799 1855 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1800 1856 wp_enqueue_script('vipps-button-webcomponent');
1801 1857 }
1802 1858
1859 + // These scripts should be loaded only on the checkout screen and is used only for the classic shortcode checkout and
1860 + // the pay-for-order screen. IOK 2026-09-15
1861 + public function enqueue_classic_checkout_scripts () {
1862 + if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) {
1863 + return;
1864 + }
1865 + // Order-pay is rendered by the classic form even with a Blocks checkout page.
1866 + // It must bypass the check for the parent checkout page's block content.
1867 + if ( ! is_checkout_pay_page() ) {
1868 + $utils = '\\Automattic\\WooCommerce\\Blocks\\Utils\\CartCheckoutUtils';
1869 + $uses_checkout_block = is_callable( array( $utils, 'is_checkout_block_default' ) )
1870 + ? $utils::is_checkout_block_default()
1871 + : has_block( 'woocommerce/checkout', wc_get_page_id( 'checkout' ) );
1803 1872
1873 + if ( $uses_checkout_block ) {
1874 + return;
1875 + }
1876 + }
1877 +
1878 + // This script uses jQuery because the classic checkout screen does too. IOK 2026-09-15
1879 + $relative_path = 'js/vipps-classic-checkout.js';
1880 + wp_enqueue_script(
1881 + 'vipps-classic-checkout',
1882 + plugins_url( $relative_path, __FILE__ ),
1883 + array( 'jquery', 'wc-checkout', 'vipps-gw' ),
1884 + filemtime( plugin_dir_path( __FILE__ ) . $relative_path ),
1885 + true
1886 + );
1887 +
1888 + if ( is_checkout_pay_page() ) {
1889 + $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
1890 + wp_add_inline_script( 'vipps-classic-checkout', 'window.VippsOrderPayConfig = ' . wp_json_encode( array(
1891 + 'orderId' => $order ? $order->get_id() : 0,
1892 + 'orderKey' => $order ? $order->get_order_key() : '',
1893 + 'billingEmail' => $order ? $order->get_billing_email() : '',
1894 + 'endpoint' => $order ? rest_url( 'wc/store/v1/checkout/' . $order->get_id() ) : '',
1895 + 'nonce' => wp_create_nonce( 'wc_store_api' ),
1896 + 'billingAddress' => $order ? $order->get_address( 'billing' ) : array(),
1897 + 'shippingAddress' => $order ? $order->get_address( 'shipping' ) : array(),
1898 + ) ) . ';', 'before' );
1899 + }
1900 + }
1901 +
1902 +
1804 1903 public function add_shortcodes() {
1805 1904 add_shortcode('woo_vipps_buy_now', array($this, 'buy_now_button_shortcode'));
1806 1905 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 1906
1809 1907 // Badges, if using shortcodes
1810 1908 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1811 1909 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
@@ -1840,8 +1938,10 @@
1840 1938 }
1841 1939
1842 1940 // Show express button option on checkout form. LP 2026-03-23
1843 1941 public function checkout_before_customer_details_express () {
1942 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1943 + do_action('woo_vipps_checkout_before_customer_details_express');
1844 1944 $gw = $this->gateway();
1845 1945 if (!$gw->show_express_checkout()) return;
1846 1946 $this->express_checkout_section_html();
1847 1947 }
@@ -1849,83 +1949,57 @@
1849 1949 public function express_checkout_section_html() {
1850 1950 $payment_method = $this->get_payment_method_name();
1851 1951 $header_text = __('Express Checkout', 'woo-vipps');
1852 1952 $header = "<legend class='express-header'>$header_text</legend>";
1853 - $div_classes = "legacy-checkout vipps-express-checkout $payment_method";
1953 + $div_classes = "legacy-checkout express $payment_method";
1854 1954 echo "<fieldset class='$div_classes'>$header";
1855 1955 $this->checkout_express_checkout_button_html();
1856 1956 echo '</fieldset>';
1857 1957 }
1858 1958
1859 - public function express_checkout_banner() {
1959 + // Show the express button if reasonable to do so
1960 + public function cart_express_checkout_button() {
1860 1961 $gw = $this->gateway();
1861 - if (!$gw->show_express_checkout()) return;
1862 - return $this->express_checkout_banner_html();
1863 - }
1864 1962
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
1963 + if ($gw->show_express_checkout()){
1964 + return $this->cart_express_checkout_button_html();
1965 + }
1882 1966 }
1883 1967
1884 - public function checkout_express_checkout_button() {
1968 + public function minicart_express_checkout_button() {
1885 1969 $gw = $this->gateway();
1886 1970
1887 1971 if ($gw->show_express_checkout()){
1888 - return $this->checkout_express_checkout_button_html();
1972 + return $this->cart_express_checkout_button_html('minicart');
1889 1973 }
1890 1974 }
1891 1975
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'));
1976 + // This is for the Vipps SDK button used instead of the normal "pay for order" and "confirm order" buttons
1977 + // on the classic checkout and pay-for-order pages. It gets swapped in when the user selects vipps, and swapped out otherwise.
1978 + public function add_checkout_button_for_classic () {
1979 + $button = $this->get_html_button_for_context('checkout');
1980 + $submit = "<div class='vipps-classic-checkout-container'><button id='vipps-classic-checkout-submit' class='hidden vipps-submit-wrapper' type='submit'>$button</button></div>";
1981 + echo $submit;
1982 + }
1983 +
1984 + public function cart_express_checkout_button_html($context= 'cart') {
1985 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1896 1986 $method = $this->get_payment_method_name();
1897 1987 $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>";
1988 + $url = "#";
1989 + $sec = wp_create_nonce('express');
1990 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
1899 1991 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1900 1992 echo $html;
1901 1993 }
1902 1994
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));
1995 + public function checkout_express_checkout_button_html() {
1996 + $button = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1925 1997 $method = $this->get_payment_method_name();
1926 1998 $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>";
1999 + $url = "#";
2000 + $sec = wp_create_nonce('express');
2001 + $html = "<a href='#' class='vipps-express-checkout short " . esc_attr($method) . "' title='" . esc_attr($title) . "' data-sec='" . esc_attr($sec) . "'>$button</a>";
1928 2002 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1929 2003 echo $html;
1930 2004 }
1931 2005
@@ -1964,19 +2038,11 @@
1964 2038 public function express_checkout_button_shortcode() {
1965 2039 $gw = $this->gateway();
1966 2040 if (!$gw->cart_supports_express_checkout()) return;
1967 2041 ob_start();
1968 - $this->cart_express_checkout_button_html('shortcode');
2042 + $this->cart_express_checkout_button_html('cart');
1969 2043 return ob_get_clean();
1970 2044 }
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 2045
1980 2046 // Manage the various product meta fields
1981 2047 public function process_product_meta ($id, $post) {
1982 2048 // This is for the 'buy now' button
@@ -2502,82 +2568,9 @@
2502 2568 if ($ok) return $callbackdir;
2503 2569 return null;
2504 2570 }
2505 2571
2506 - // Unfortunately, we cannot do any form of portable locking, and we may get callbacks from Vipps arriving at the same moment as we check the status at Vipps,
2507 - // which in the very worst case, for Express Checkout orders, may lead to a double shipping line. Changing this to a queue system is non-trivial, because some of
2508 - // the operations done when modifying the order actually requires the customers session to be active. This operation will make conflicts a litte less probable
2509 - // by implementing something that isn't quite a lock, and the filter may be used to implement proper locking, using e.g. flock, where this can be used
2510 - // (non-distributed environments using unix on standard filesystems. IOK 2020-05-15
2511 - // Returns true if lock succeeds, or false.
2512 - public function lockOrder($order) {
2513 - $orderid = $order->get_id();
2514 - if (has_filter('woo_vipps_lock_order')) {
2515 - $ok = apply_filters('woo_vipps_lock_order', $order);
2516 - if (!$ok) return false;
2517 - } else {
2518 - if(get_transient('order_lock_'.$orderid)) return false;
2519 - $this->lockKey = uniqid();
2520 - set_transient('order_lock_' . $orderid, $this->lockKey, 30);
2521 - }
2522 - add_action('shutdown', function () use ($order) { global $Vipps; $Vipps->unlockOrder($order); });
2523 - return true;
2524 - }
2525 - // If the order is locked, it means it is in the process of being finalized, so for instance, we do *not* want to abandon it
2526 - // in checkout.
2527 - public function isLocked ($order) {
2528 - $orderid = $order->get_id();
2529 - $locked = get_transient('order_lock_'.$orderid);
2530 - return apply_filters('woo_vipps_order_locked', $locked, $order);
2531 - }
2532 - public function unlockOrder($order) {
2533 - $orderid = $order->get_id();
2534 - if (has_action('woo_vipps_unlock_order')) {
2535 - do_action('woo_vipps_unlock_order', $order);
2536 - } else {
2537 - if(get_transient('order_lock_'.$orderid) == $this->lockKey) {
2538 - delete_transient('order_lock_'.$orderid);
2539 - }
2540 - }
2541 - }
2542 2572
2543 - // Functions using flock() and files to lock orders. This is only guaranteed to work on certain setups, ie, non-distributed setups
2544 - // using Unix with normal filesystems (not NFS).
2545 - public function flock_lock_order($order) {
2546 - global $_orderlocks;
2547 - if (!$_orderlocks) $_orderlocks = array();
2548 - $dir = $this->callbackDir();
2549 - if (!$dir) {
2550 - $this->log(__("Cannot use flock() to lock orders: cannot create or write to directory", "woo-vipps"), 'error');
2551 - return true;
2552 - }
2553 - $fname = '.ht-vipps-lock-'.md5($order->get_order_key() . $order->get_meta('_vipps_transaction'));
2554 - $path = $dir . DIRECTORY_SEPARATOR . $fname;
2555 - touch($path);
2556 - if (!is_writable($path)) {
2557 - $this->log(__("Cannot use flock() to lock orders: cannot create lockfiles ", "woo-vipps"), 'error');
2558 - return true;
2559 - }
2560 - $handle = fopen($path, 'w+');
2561 - if (flock($handle, LOCK_EX | LOCK_NB)) {
2562 - $_orderlocks[$order->get_id()] = array($handle,$path);
2563 - return true;
2564 - }
2565 - return false;
2566 - }
2567 - public function flock_unlock_order($order) {
2568 - $orderid=$order->get_id();
2569 - global $_orderlocks;
2570 - if (!$_orderlocks) return;
2571 - if (!isset($_orderlocks[$orderid])) return;
2572 - list($handle, $path) = $_orderlocks[$orderid];
2573 - unset($_orderlocks[$orderid]);
2574 - flock($handle, LOCK_UN);
2575 - fclose($handle);
2576 - @unlink($path);
2577 - }
2578 -
2579 -
2580 2573 // Because the prefix used to create the Vipps order id is editable
2581 2574 // by the user, we will store that as a meta and use this for callbacks etc.
2582 2575 // IOK 2023-01-23 this function is no longer used, and kept only for backwards compatibility with
2583 2576 // debug filters and similar.
@@ -2647,12 +2640,36 @@
2647 2640 remove_filter('template_redirect', 'redirect_canonical', 10);
2648 2641 // dont cache special page. LP 2026-08-25
2649 2642 $this->nocache();
2650 2643 // Do the custom pre-load actions for these pages IOK 2026-09-11
2651 - do_action('woo_vipps_before_handling_special_page', $_GET['action']);
2644 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2652 2645 }
2653 2646 }
2654 2647
2648 + // Ran in template redirect for the special page. IOK 2026-09-2
2649 + public function pre_special_page_actions ($action) {
2650 + // Change title dynamically depending on action. LP 2026-09-02
2651 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2652 +
2653 + // If we are handling the 'wait for payment' action, we need to poll the order status before
2654 + // we start producing content IOK 2026-09-21
2655 + if ($action == 'wait_for_payment') {
2656 + $this->handle_payment_poll_and_redirect();
2657 + }
2658 +
2659 + // Some validation is required for this action
2660 + if ($action == 'do_express_checkout') {
2661 + $this->vipps_express_checkout_consistency_check();
2662 + }
2663 + // These two actions require an extra script
2664 + if (in_array($action, ['buy_product','do_express_checkout'])) {
2665 + wp_enqueue_script('vipps-purchase', plugins_url('js/vipps-purchase.js',__FILE__), ['vipps-gw'],
2666 + filemtime(dirname(__FILE__) . "/js/vipps-purchase.js"),
2667 + ['in_footer'=>true]
2668 + );
2669 + }
2670 + }
2671 +
2655 2672 // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2656 2673 public function vipps_special_page_endpoint_title($title, $postid = 0) {
2657 2674 global $wp_query;
2658 2675 // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
@@ -2795,14 +2812,18 @@
2795 2812 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2796 2813 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2797 2814
2798 2815 // 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
2816 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2800 2817 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2801 2818
2802 2819 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2803 2820 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2804 2821
2822 + // For the classic checkout page and pay-for-order page, use a custom submit button when payment method
2823 + // is Vipps
2824 + add_action('woocommerce_review_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2825 + add_action('woocommerce_pay_order_after_submit', array($this, 'add_checkout_button_for_classic'));
2805 2826
2806 2827 // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2807 2828 add_action('template_redirect', array($this,'template_redirect'),1);
2808 2829
@@ -2812,21 +2833,8 @@
2812 2833 // Ajax endpoints for checking the order status while waiting for confirmation
2813 2834 add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status'));
2814 2835 add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status'));
2815 2836
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 2837 // Handle the cancel unpaid order action when the "hold stock" times out.
2830 2838 // For *normal* vipps orders, we run another cronjob every 5. minute which checks order status,
2831 2839 // therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return.
2832 2840 // For Checkout the rules are different though.
@@ -2905,9 +2913,8 @@
2905 2913 $this->vippsJSConfig = array();
2906 2914 $this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php');
2907 2915 $this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps');
2908 2916 $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 2917 $this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__);
2911 2918 $this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name());
2912 2919 $this->vippsJSConfig['vippsbuynowdescription'] = sprintf(__( 'Add a %1$s Buy Now-button to the product block or choose a product manually', 'woo-vipps'), $this->get_payment_method_name());
2913 2920 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
@@ -2912,10 +2919,17 @@
2912 2919 $this->vippsJSConfig['vippsbuynowdescription'] = sprintf(__( 'Add a %1$s Buy Now-button to the product block or choose a product manually', 'woo-vipps'), $this->get_payment_method_name());
2913 2920 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2914 2921 $this->vippsJSConfig['vippslocale'] = get_locale();
2915 2922 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
2916 -
2923 + $this->vippsJSConfig['paymentMethodSlug'] = sanitize_title($this->get_payment_method_name());
2924 + $this->vippsJSConfig['paymentMethodName'] = $this->get_payment_method_name();
2925 + $wc_lang = $this->get_html_button_attrs_for_context()['language'];
2926 + if ('store' === $wc_lang) $wc_lang = $this->get_customer_language();
2927 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
2928 + if ('dk' === $wc_lang) $wc_lang = 'da';
2929 + $this->vippsJSConfig['webcomponentLanguage'] = $wc_lang;
2917 2930
2931 +
2918 2932 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2919 2933 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
2920 2934 // Ensure gateways are loaded at this point IOK 2026-05-27
2921 2935 require_once(dirname(__FILE__) . '/WC_Gateway_VippsCard.class.php');
@@ -3787,9 +3801,9 @@
3787 3801 $shipping_method = $methodclass ? new $methodclass($rate->get_instance_id()) : null;
3788 3802
3789 3803 $tax = $rate->get_shipping_tax() ?: 0;
3790 3804 $cost = $rate->get_cost() ?: 0;
3791 - $label = $rate->get_label();
3805 + $label = html_entity_decode($rate->get_label());
3792 3806
3793 3807 if ($cost == 0 && ($methodid != 'local_pickup' && $methodid != 'pickup_location')) {
3794 3808 $has_free_shipping = true;
3795 3809 }
@@ -3815,9 +3829,9 @@
3815 3829 $shippingcostB = sprintf("%.2F",wc_format_decimal($cost, '') + wc_format_decimal($tax,''));
3816 3830 $shippingcost = max($shippingcostA, $shippingcostB);
3817 3831
3818 3832 $vippsmethod['shippingCost'] = $shippingcost;
3819 - $vippsmethod['shippingMethod'] = $rate->get_label();
3833 + $vippsmethod['shippingMethod'] = html_entity_decode($rate->get_label());
3820 3834 $vippsmethod['shippingMethodId'] = $key;
3821 3835 $vippsmethods[]=$vippsmethod;
3822 3836
3823 3837 // Metadata and settings stored for later use for Checkout
@@ -4131,9 +4145,9 @@
4131 4145 WC()->cart->calculate_totals();
4132 4146 WC()->cart->set_session();
4133 4147 return true;
4134 4148 } catch (Exception $e) {
4135 - $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error');
4149 + $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->getMessage()), 'error');
4136 4150 return false;
4137 4151 }
4138 4152 }
4139 4153
@@ -4162,9 +4176,9 @@
4162 4176 $tax = $rate->get_shipping_tax() ?: 0;
4163 4177 $cost = $rate->get_cost() ?: 0;
4164 4178
4165 4179 $method['shippingCost'] = sprintf("%.2F",wc_format_decimal($cost+$tax,''));
4166 - $method['shippingMethod'] = $rate->get_label();
4180 + $method['shippingMethod'] = html_entity_decode($rate->get_label());
4167 4181 // We may not really need the tax stashed here, but just to be sure.
4168 4182 $method['shippingMethodId'] = $rate->get_id() . ";" . $tax;
4169 4183 $methods[]= $method;
4170 4184
@@ -4243,9 +4257,11 @@
4243 4257 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
4244 4258 return $url;
4245 4259 }
4246 4260 $url = $this->express_checkout_url();
4247 - $url = wp_nonce_url($url,'express','sec');
4261 + // At this point, there is always a query argument here. IOK 2026-09-21
4262 + $nonce = wp_create_nonce('express');
4263 + $url = $url . "&sec=$nonce";
4248 4264
4249 4265 return $url;
4250 4266 }
4251 4267
@@ -4442,28 +4458,12 @@
4442 4458 $order = wc_get_order($order->get_id());
4443 4459 $order_status = $order->get_status();
4444 4460
4445 4461 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 - }
4462 +
4463 + $gw = $this->gateway();
4464 4464 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4465 - return $this->check_status_of_pending_order($order);
4465 + $newstatus = $gw->poll_and_check_order_status($order);
4466 4466 }
4467 4467
4468 4468 // In some situations we have to empty the cart when the user goes to Vipps, so
4469 4469 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
@@ -4531,8 +4531,9 @@
4531 4531
4532 4532 // Maybe log in user
4533 4533 // It is done on the thank-you page of the order, and only for express checkout.
4534 4534 function maybe_log_in_user ($order) {
4535 +
4535 4536 if (is_user_logged_in()) return;
4536 4537 if (!$order || ! self::is_vipps_order($order)) return;
4537 4538
4538 4539 // We *do* want to log in express checkout customers, but not those that
@@ -4678,129 +4679,233 @@
4678 4679 }
4679 4680 if (!$o) return;
4680 4681 if (!$o->get_meta('_vipps_single_product_express')) return;
4681 4682 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4682 - if ($failed) WC()->cart->empty_cart();
4683 + // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4684 + WC()->cart->empty_cart();
4683 4685 $this->restore_cart($o);
4684 4686 }
4685 4687
4686 4688
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);
4689 + // Actually create a express checkout order object, with no shipping or personal information, returning information about
4690 + // the result. The order should at this point be in a/the cart. For single product purchases, this is a different cart than
4691 + // the main one; for cart purchases, it's just the WC()->cart object. IOK 2026-08-25
4692 + private function create_and_process_express_order() {
4693 + $result = null;
4694 + $gw = $this->gateway();
4695 + try {
4696 + $orderid = $gw->create_partial_order();
4697 + do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4698 + } catch (Exception $e) {
4699 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4700 + return $result;
4701 + }
4702 + if (!$orderid) {
4703 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4704 + return $result;
4695 4705 }
4696 - $session->set('__vipps_buy_product', json_encode($_REQUEST));
4697 4706
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());
4707 + try {
4708 + $this->maybe_add_static_shipping($gw,$orderid);
4709 + } catch (Exception $e) {
4710 + $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4711 + $this->log($e->getMessage(),'error');
4712 + $result = array('ok'=>0, 'orderid'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4713 + return $result;
4714 + }
4701 4715
4702 - $result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url);
4703 - wp_send_json($result);
4704 - exit();
4716 + // Now pass this to the Woo gateway and get a redirect URL back IOK 2026-08-25
4717 + $ok = $gw->process_payment($orderid);
4718 + if ($ok && $ok['result'] == 'success') {
4719 + $result = array('ok'=>1, 'orderid'=>$orderid, 'msg'=>'', 'url'=>$ok['redirect']);
4720 + return $result;
4721 + }
4722 + $result = array('ok'=>0, 'orderid'=>$orderid, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4723 + return $result;
4705 4724 }
4706 4725
4707 - public function ajax_do_express_checkout () {
4708 - check_ajax_referer('do_express','sec');
4709 - Vipps::nocache();
4710 - static::set_locale_if_in_header();
4726 + // This creates a simple hash for the 'current order' which we will store in the session if we proceed to checkout. We use this to
4727 + // avoid/warn the user of duplicate purchases. IOK 2026-09-09
4728 + public function create_order_hash($args=null) {
4729 + // If we have no arguments, we'll hash the cart.
4730 + if (empty($args)) {
4731 + $cartitems = WC()->cart->get_cart();
4732 + $orderspec = array();
4733 + foreach($cartitems as $item => $values) {
4734 + $orderspec[] = array('sku'=> ($values['sku'] ?? ""), 'product_id'=>($values['product_id'] ?? 0), 'variation_id'=>($values['variation_id'] ?? 0), 'quantity'=>($values['quantity'] ?? 1));
4735 + }
4736 + $args = $orderspec;
4737 + }
4738 + return md5(serialize($args));
4739 + }
4740 +
4741 +
4742 + // This method may provide HTML form elements to ask a user questions after starting
4743 + // express checkout. It is used to detect duplicate orders, possibly for terms and conditions, and user-definiable customizations. IOK 2026-09-09
4744 + // NULL productinfo means use the cart; the "current hash" is used to detect duplicates, and is calculated by the caller.
4745 + public function express_order_needs_confirmation($args, $productinfo, $current_hash) {
4746 + $elements = [];
4747 + $html = "";
4748 +
4749 + // First, let's check if we need to confirm the purchase.
4750 + $last_express_purchase_hash = WC()->session->get('woo_vipps_last_express');
4751 + if ($last_express_purchase_hash) {
4752 + list($hash, $orderid, $stamp) = explode(":", $last_express_purchase_hash);
4753 + $cutoff = $stamp + apply_filters('woo_vipps_recent_order_cutoff', (3*60));
4754 + if ($hash == $current_hash && (time() <= $cutoff )) {
4755 + $order = wc_get_order($orderid);
4756 + $status = $order ? $order->get_status() : false;
4757 + // IOK TODO/FIXME actually, if the order is pending/failed/cancelled and *identical* to our current productinfo, we could plausibly do a restart here. Would probably require careful checking though, and
4758 + // a different flow. IOK 2026-09-17
4759 + if (in_array($status, ['on-hold', 'processing', 'completed'])) {
4760 + $header = __("Are you sure?",'woo-vipps');
4761 + $body = __("You recently completed an order with exactly the same products as you are buying now. There should be an email in your inbox from the previous purchase. Are you sure you want to order again?",'woo-vipps');
4762 + $elements['possible_duplicate'] = "<h1>$header</h1><p>$body</p>";
4763 + $this->log(__("It seems a customer is trying to re-order product(s) recently bought in the same session, asking user for confirmation", 'woo-vipps'), 'info');
4764 + }
4765 + }
4766 + }
4767 +
4711 4768 $gw = $this->gateway();
4769 + $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
4770 + $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
4771 + $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
4712 4772
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();
4773 + if ($askForTerms) {
4774 + $termsHTML = '';
4775 + // Include shop terms
4776 + ob_start();
4777 + wc_get_template('checkout/terms.php');
4778 + $termsHTML = ob_get_clean();
4779 + $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
4780 + $elements['terms'] = $termsHTML;
4717 4781 }
4718 4782
4783 + // Custom fields
4784 + ob_start();
4785 + do_action('woo_vipps_express_checkout_orderspec_form', $productinfo, $args);
4786 + $extra_fields = ob_get_clean();
4787 + if (!empty($extra_fields)) {
4788 + $elements['extra'] = $extra_fields;
4789 + }
4719 4790
4720 -
4791 + if (!empty($elements)) {
4792 + $html = join("\n", array_values($elements));
4793 + $msg = join(",", array_keys($elements));
4794 + return ['ok'=>2, 'msg'=>$msg, 'html'=>$html, 'url'=>''];
4795 + }
4721 4796
4797 + return false;
4798 +
4799 + }
4800 +
4801 + public function rest_do_express_checkout ($request) {
4802 + Vipps::nocache();
4803 + check_ajax_referer('express', 'sec');
4804 + static::set_locale_if_in_header();
4805 + $args = $request->get_json_params();
4806 + if (!$args) {
4807 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4808 + }
4809 +
4810 + // Since this is the REST api, we need to load the cart manually here. IOK 2026-08-27
4811 + if ( is_null( WC()->cart ) ) {
4812 + WC()->frontend_includes();
4813 + if ( ! WC()->session instanceof WC_Session ) {
4814 + WC()->session = new WC_Session_Handler();
4815 + WC()->session->init();
4816 + }
4817 + if (is_null( WC()->customer)) {
4818 + WC()->customer = new WC_Customer( get_current_user_id(), true );
4819 + }
4820 + WC()->cart = new WC_Cart();
4821 + WC()->cart->get_cart_from_session();
4822 + }
4823 +
4824 +
4825 + $gw = $this->gateway();
4826 + if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4827 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4828 + return $result;
4829 + }
4722 4830 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4723 4831 $toolate = false;
4724 4832 $msg = "";
4725 4833 $valid = WC()->cart->check_cart_item_validity();
4726 4834 if ( is_wp_error( $valid) ) {
4727 - $toolate = true;
4728 - $msg = "<br>" . $valid->get_error_message();
4835 + $toolate = true;
4836 + $msg = "<br>" . $valid->get_error_message();
4729 4837 }
4730 4838 $stock = WC()->cart->check_cart_item_stock();
4731 - if ( is_wp_error( $stock) ) {
4732 - $toolate = true;
4733 - $msg = "<br>" . $stock->get_error_message();
4734 - }
4839 + if ( is_wp_error( $stock) ) {
4840 + $toolate = true;
4841 + $msg = "<br>" . $stock->get_error_message();
4842 + }
4735 4843
4736 4844 if ($toolate) {
4737 4845 $result = array('ok'=>0, 'msg'=>sprintf(__('Some of the products in your cart are no longer available in the quantities you have ordered. Please <a href="%1$s">edit your order</a> before continuing the checkout','woo-vipps'), wc_get_cart_url()) . $msg, 'url'=>false);
4738 - wp_send_json($result);
4739 - exit();
4846 + return $result;
4740 4847 }
4741 4848
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();
4849 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4850 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4851 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4852 + $cookies = $args['cookies'] ?? [];
4853 + foreach($cookies as $key => $value) {
4854 + if (!isset($_COOKIE[$key])) {
4855 + $_COOKIE[$key] = $value;
4856 + }
4755 4857 }
4858 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4859 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4860 + $others =$args['post'] ?? [];
4861 + foreach($args['post'] as $key=>$value) {
4862 + $_POST[$key] = $value;
4863 + }
4756 4864
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();
4865 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4866 + $current_hash = $this->create_order_hash();
4867 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4868 + if (!$confirmation) {
4869 + $result = $this->express_order_needs_confirmation($args, null, $current_hash);
4870 + if (!empty($result)) {
4871 + return $result;
4872 + }
4765 4873 }
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();
4874 +
4875 + $result = $this->create_and_process_express_order();
4876 + if ($result['ok'] == 1) {
4877 + $orderid = $result['orderid'];
4878 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4879 + WC()->session->save_data();
4772 4880 }
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();
4881 + return $result;
4882 +
4776 4883 }
4777 4884
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();
4885 +
4886 + // Rest handler for single product express checkout. Expects arguments as JSON. IOK 2026-08-25
4887 + public function rest_do_single_product_express_checkout ($request) {
4888 + Vipps::nocache();
4782 4889 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();
4890 + $args = $request->get_json_params();
4891 + if (!$args) {
4892 + return new WP_Error('no_data', __('No data passed to express checkout', 'woo-vipps'), ['status' => 400]);
4790 4893 }
4894 + $result = ['ok' => 0, 'msg'=>'', 'orderid'=>0, 'url'=>''];
4791 4895
4896 + // We receive the varid, prodid, sku and quantity directly. One of these. The sku is the dominant one. IOK 2026-08-27
4897 + $varid = intval($args['variation_id'] ?? 0);
4898 + $prodid = intval($args['product_id'] ?? 0);
4899 + $sku = sanitize_text_field($args['sku'] ?? "");
4900 + $quantity = max(1, intval($args['quantity'] ?? 0));
4792 4901
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 4902
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 ) {
4903 + // We expect the variations - that is, the fields named "attribute_..." to be sent as post fields.
4904 + // We just need to sanitize them.
4905 + $variations = [];
4906 + $invars = $args['post'] ?? [];
4907 + foreach ($invars as $key => $value) {
4803 4908 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4804 4909 continue;
4805 4910 }
4806 4911 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
@@ -4805,15 +4910,94 @@
4805 4910 }
4806 4911 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4807 4912 }
4808 4913
4809 - $product = null;
4810 - $variant = null;
4811 - $parent = null;
4812 - $parentid = null;
4813 - $quantity = 1;
4814 - if ($quant && $quant>1) $quantity=$quant;
4914 + // Then the cookies. These would be the _ga and sbjs_ cookies typically, but we'll let users handle these themselves.
4915 + // These are passed as arguments from the javascript, since proxies are likely to strip them. This should allow
4916 + // systems like MonsterInsights that look for the _GA cookie to succeed. IOK 2026-08-30
4917 + $cookies = $args['cookies'] ?? [];
4918 + foreach($cookies as $key => $value) {
4919 + if (!isset($_COOKIE[$key])) {
4920 + $_COOKIE[$key] = $value;
4921 + }
4922 + }
4815 4923
4924 + // There might be extra values here now, which would typically have been posted as POST arguments, in a form.
4925 + // User-defined stuff and so on. We'll initiate the POST value with these to simulate this for backwards compatibility.
4926 + $others =$args['post'] ?? [];
4927 + foreach($args['post'] as $key=>$value) {
4928 + $_POST[$key] = $value;
4929 + }
4930 +
4931 + // Since this is the REST api, we need to load the cart manually here. *Not* loading the cart could be an option but unpredictable. IOK 2026-08-27
4932 + if ( is_null( WC()->cart ) ) {
4933 + WC()->frontend_includes();
4934 + if ( ! WC()->session instanceof WC_Session ) {
4935 + WC()->session = new WC_Session_Handler();
4936 + WC()->session->init();
4937 +
4938 + // If we don't have a session cookie, we need to set it, and also initialize the $_COOKIE value. IOK 2026-09-29
4939 + if (! WC()->session->get_session_cookie()) {
4940 + $store_session_cookie = function ( $options, $name, $value ) { $_COOKIE[$name] = $value; return $options;};
4941 + add_filter('woocommerce_set_cookie_options', $store_session_cookie, 10, 3);
4942 + try {
4943 + WC()->session->set_customer_session_cookie( true ); // We have to explicitly set the cookie if this session is fresh. IOK 2026-09-29
4944 + } finally {
4945 + remove_filter('woocommerce_set_cookie_options', $store_session_cookie, 10);
4946 + }
4947 + }
4948 + }
4949 + if (is_null( WC()->customer)) {
4950 + WC()->customer = new WC_Customer( get_current_user_id(), true );
4951 + }
4952 + WC()->cart = new WC_Cart();
4953 + WC()->cart->get_cart_from_session();
4954 + }
4955 +
4956 + // Try to avoid re-purchasing the same order repeatedly. IOK 2026-09-02
4957 + // We calculate this here so we can add it to the session later. IOK 2026-09-09
4958 + $orderspec = array('sku'=> $sku, 'product_id'=>$prodid, 'variation_id'=>$varid, 'quantity'=>$quantity);
4959 + $current_hash = $this->create_order_hash($orderspec);
4960 +
4961 + // Now to handle "extra questions" for an order, including terms + conditions and "possible duplicate order" IOK 2026-09-09
4962 + $confirmation = (bool) intval(($others['confirmed'] ?? 0));
4963 + if (!$confirmation) {
4964 + $result = $this->express_order_needs_confirmation($args, $orderspec, $current_hash);
4965 + if (!empty($result)) {
4966 + $response = new WP_REST_Response($result);
4967 + $response->set_status(200);
4968 + return $response;
4969 + }
4970 + }
4971 +
4972 + // Basically always return 200 after this, and always return an object with an 'ok' and a 'msg' value, possibly 'orderid' and 'url'.
4973 + $result = $this->really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity, $variations);
4974 + // And if we're going to express now so let's note the order. IOK 2026-08-27. Now this assumes success, but *basically* I think this is ok.
4975 + // We'll reset it on order failure I think. IOK 2026-08-20 FIXME
4976 + if ($result['ok'] == 1) {
4977 + $orderid = $result['orderid'];
4978 + WC()->session->set('woo_vipps_last_express', "$current_hash:$orderid:" . time());
4979 + WC()->session->save_data();
4980 + }
4981 +
4982 + $response = new WP_REST_Response($result);
4983 + $response->set_status(200);
4984 +
4985 + return $response;
4986 + }
4987 +
4988 + // Common private method to do single product express checkout, used by the new REST express. IOK 2026-08-25
4989 + private function really_do_single_product_express_checkout($prodid, $varid, $sku, $quantity=1, $variations=[]) {
4990 + require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4991 + $gw = $this->gateway();
4992 +
4993 + if (!$gw->express_checkout_available()) {
4994 + $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4995 + return $result;
4996 + }
4997 + // Here we will either have a product-id, a variant-id and a product-id, or just a SKU. The product-id will not be a variant - but
4998 + // we'll double-check just in case. Also if we somehow *just* get a variant-id we should fix that too. But a SKU trumps all. IOK 2018-10-02
4999 +
4816 5000 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
4817 5001 // Moved rules around as the _sku variant broke in 3.6.1 for stores that didn't bother to update the database IOK 2019-04-24
4818 5002 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
4819 5003 try {
@@ -4826,17 +5010,14 @@
4826 5010 $product = wc_get_product($skuid);
4827 5011 }
4828 5012 } catch (Exception $e) {
4829 5013 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
4830 - wp_send_json($result);
4831 - exit();
5014 + return $result;
4832 5015 }
4833 5016
4834 -
4835 5017 if (!$product) {
4836 5018 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
4837 - wp_send_json($result);
4838 - exit();
5019 + return $result;
4839 5020 }
4840 5021
4841 5022 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
4842 5023 $parent = $parentid ? wc_get_product($parentid) : null;
@@ -4843,34 +5024,30 @@
4843 5024
4844 5025 // This can't really happen, but if it did..
4845 5026 if ($prodid && $parentid && ($prodid != $parentid)) {
4846 5027 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
4847 - wp_send_json($result);
4848 - exit();
5028 + return $result;
4849 5029 }
4850 5030 if (!$gw->product_supports_express_checkout($product)) {
4851 5031 $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();
5032 + return $result;
4854 5033 }
4855 5034
4856 5035 // Somebody addded the wrong SKU
4857 5036 if ($product->get_type() == 'variable'){
4858 5037 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
4859 - wp_send_json($result);
4860 - exit();
5038 + return $result;
4861 5039 }
4862 5040 // Final check of availability
4863 5041 if (!$product->is_purchasable() || !$product->is_in_stock()) {
4864 5042 $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();
5043 + return $result;
4867 5044 }
4868 5045
4869 5046 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
4870 -
4871 5047 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
4872 5048 // because some plugins actually override this.
5049 + // NB: Please note the cart must have been loaded here, be aware when doing REST. IOK 2026-08-27
4873 5050 $current_cart = clone WC()->cart;
4874 5051 WC()->cart->empty_cart();
4875 5052
4876 5053 if ($parent && $parent->get_type() == 'variable') {
@@ -4877,50 +5054,22 @@
4877 5054 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
4878 5055 } else {
4879 5056 WC()->cart->add_to_cart($product->get_id(),$quantity);
4880 5057 }
5058 + WC()->session->save_data();
4881 5059
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 - }
5060 + $result = $this->create_and_process_express_order();
4890 5061
4891 - if (!$orderid) {
4892 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4893 - wp_send_json($result);
4894 - exit();
5062 + if ($result['ok'] ?? false) {
5063 + // Single product purchase, so save any contents of the real cart
5064 + $orderid = $result['orderid'];
5065 + $order = wc_get_order($orderid);
5066 + $order->update_meta_data('_vipps_single_product_express',true);
5067 + $order->save();
5068 + $this->save_cart($order,$current_cart);
4895 5069 }
4896 5070
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();
5071 + return $result;
4923 5072 }
4924 5073
4925 5074 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
4926 5075 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
@@ -4986,9 +5135,9 @@
4986 5135 $transaction = sanitize_text_field(@$_POST['transaction']);
4987 5136
4988 5137 $sessionorders= WC()->session->get('_vipps_session_orders');
4989 5138 if (!isset($sessionorders[$orderid])) {
4990 - wp_send_json(array('status'=>'error', 'msg'=>__('Not an order','woo-vipps')));
5139 + wp_send_json(array('status'=>'error', 'msg'=>__('Not a session order','woo-vipps')));
4991 5140 }
4992 5141
4993 5142 $order = wc_get_order($orderid);
4994 5143 if (!$order) {
@@ -5245,9 +5394,8 @@
5245 5394 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5246 5395 }
5247 5396
5248 5397
5249 -
5250 5398 // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5251 5399 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5252 5400 // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5253 5401 public function woocommerce_create_pages ($data) {
@@ -5299,10 +5447,12 @@
5299 5447 // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5300 5448 if (is_admin()) return;
5301 5449 if (wp_doing_ajax()) return;
5302 5450 if (defined('REST_REQUEST') && REST_REQUEST) return;
5451 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5303 5452
5304 5453 $action = $_GET['action'] ?? '';
5454 + $html = "";
5305 5455 switch ($action) {
5306 5456 case 'wait_for_payment':
5307 5457 $html = $this->vipps_wait_for_payment();
5308 5458 break;
@@ -5314,8 +5464,10 @@
5314 5464 break;
5315 5465 default:
5316 5466 $html = '';
5317 5467 }
5468 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5469 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5318 5470
5319 5471 // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5320 5472 return $html;
5321 5473 }
@@ -5324,14 +5476,8 @@
5324 5476 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5325 5477 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5326 5478 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5327 5479 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 5480 do_action('woo_vipps_express_checkout_page');
5335 5481
5336 5482 $session = WC()->session;
5337 5483 $posted = $session->get('__vipps_buy_product');
@@ -5365,32 +5511,33 @@
5365 5511 }
5366 5512
5367 5513 // Pass the productinfo to the express checkout form
5368 5514 $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']);
5515 + $args['product_id'] = esc_attr(intval($productinfo['product_id'] ?? 0));
5516 + $args['variation_id'] = esc_attr(intval($productinfo['variation_id'] ?? 0));
5517 + $args['sku'] = esc_attr(sanitize_text_field($productinfo['product_sku'] ?? ""));
5518 + $args['quantity'] = esc_attr(max(1, intval($productinfo['quantity'] ?? 0)));
5374 5519
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 - }
5520 + $payment_method = $this->get_payment_method_name();
5521 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5522 + $bclass = esc_attr($payment_method);
5382 5523
5383 - return $this->express_checkout_page_html(true,'do_single_product_express_checkout',$args);
5524 + $content = "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5525 + $content .= "<div class='vipps-qr-purchase' style='visibility:hidden'>";
5526 + $content .= "<a href='javascript:void(0)' class='single-product button vipps-buy-now $bclass' data-vipps-autostart='true' data-vipps-purchase='single' data-product_id='{$args['product_id']}' data-variation_id='{$args['variation_id']}' data-product_sku='{$args['sku']}' data-quantity='{$args['quantity']}' title='{$btitle}';
5527 + >";
5528 + $content .= $this->get_html_button_for_context('global');
5529 + $content .= "</a>";
5530 + $content .= "</div>";
5531 +
5532 + return $content;
5384 5533 }
5385 5534
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() {
5535 + public function vipps_express_checkout_consistency_check() {
5388 5536 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5389 5537 // IOK 2018-05-28
5390 5538 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5391 5539
5392 -
5393 5540 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5394 5541 if (!$backurl) $backurl = home_url();
5395 5542
5396 5543 if (!$ok) {
@@ -5404,215 +5551,39 @@
5404 5551 wp_redirect($backurl);
5405 5552 exit();
5406 5553 }
5407 5554
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');
5555 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5417 5556 }
5418 5557
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 - }
5558 + // This is a landing page for the express checkout of the normal cart - it is done like this because this could take time on slower hosts.
5559 + // IOK 2026-09-09 - nowadays this is only used for compatibility mode. It will automatically start express checkout of the current cart when reached.
5560 + public function vipps_express_checkout() {
5561 + // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5562 + if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5563 + $content = __('Link expired, please try again', 'woo-vipps');
5564 + return $content;
5439 5565 }
5440 - // Nothing more to do here
5441 - if (empty($sessionorders)) return true;
5566 +
5567 + do_action('woo_vipps_express_checkout_page');
5442 5568
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 - }
5569 + $payment_method = $this->get_payment_method_name();
5570 + $btitle = esc_attr(sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method));
5571 + $bclass = esc_attr($payment_method);
5572 + $sec = esc_attr($_REQUEST['sec']);
5573 + $content = "";
5574 + $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "...</p>";
5575 + $content .= '<div class="vipps-cart-purchase" style="visibility:hidden">"';
5576 + $content .= "<a href='javascript:void(0)' class='vipps-express-checkout short $bclass' data-vipps-autostart='true' data-sec='$sec' title='$btitle'>";
5577 + $content .= $this->get_html_button_for_context('global');
5578 + $content .="</a>";
5579 + $content .="</div>";
5460 5580
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;
5581 + return $content;
5472 5582 }
5473 5583
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() {
5584 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5585 + private function handle_payment_poll_and_redirect () {
5615 5586 $orderid = WC()->session->get('_vipps_pending_order');
5616 5587
5617 5588 $order = null;
5618 5589 $gw = $this->gateway();
@@ -5626,9 +5597,9 @@
5626 5597 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5627 5598 // simulating the session with that.
5628 5599 // IOK 2019-11-19, changed to using GET 2023-01-23
5629 5600 if ($no_session && $limited_session) {
5630 - $orderid = intval(@$_GET['id']);
5601 + $orderid = intval($_GET['id'] ?? false);
5631 5602 }
5632 5603 if ($orderid) {
5633 5604 clean_post_cache($orderid);
5634 5605 $order = wc_get_order($orderid);
@@ -5643,19 +5614,22 @@
5643 5614 $session = WC()->session;
5644 5615 if (!$session->has_session()) {
5645 5616 $session->set_customer_session_cookie(true);
5646 5617 }
5618 +
5619 + $sessionorders= WC()->session->get('_vipps_session_orders');
5620 + $sessionorders[$orderid] = 1;
5621 + WC()->session->set('_vipps_session_orders',$sessionorders);
5647 5622 $session->set('_vipps_pending_order', $orderid);
5623 + WC()->session->save_data();
5648 5624 }
5649 5625 }
5650 5626
5651 - do_action('woo_vipps_wait_for_payment_page',$order);
5652 -
5653 5627 $deleted_order=0;
5654 5628 if ($orderid && !$order) {
5655 5629 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5656 5630 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
5657 - $this->log(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), 'debug');
5631 + $this->log(sprintf(__("In order return: The order %1\$d seems to be deleted", 'woo-vipps'), $orderid), 'debug');
5658 5632 $deleted_order=1;
5659 5633 }
5660 5634
5661 5635 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
@@ -5665,12 +5639,13 @@
5665 5639
5666 5640 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5667 5641 $do_poll = true;
5668 5642
5669 - // Still pending, no callback. Make a call to the server as the order might not have been created. IOK 2018-05-16
5643 + // Do a single poll here to check and set the order status at Woo using the order status at Vipps IOK 2026-09-29
5670 5644 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);
5645 + // We will do *one* poll before waiting for the callback (for a while, at least.) IOK 2026-09-29
5646 + $newstatus = $gw->poll_and_check_order_status($order);
5647 + $this->log(sprintf(__("In order return: Order status of %1\$d is %2\$s", 'woo-vipps'), $orderid, $newstatus), 'info');
5673 5648 if ($status != $newstatus) {
5674 5649 $status = $newstatus;
5675 5650 clean_post_cache($orderid);
5676 5651 $order = wc_get_order($orderid); // Reload order object
@@ -5675,11 +5650,13 @@
5675 5650 clean_post_cache($orderid);
5676 5651 $order = wc_get_order($orderid); // Reload order object
5677 5652 }
5678 5653 } else {
5679 - // No need to do anyting here. IOK 2020-01-26
5654 + // No need to do anyting here. IOK 2020-01-26
5680 5655 }
5681 5656
5657 + // Actually, this may cause a second poll if the first left us pending. Should be rewritten - but *mostly* it will just check
5658 + // the payment status at Vipps without polls, which will tell us if the payment succeeded in case people use custom order statuses and so on. IOK 2026-09-29
5682 5659 $payment = 'notchecked';
5683 5660 if ($do_poll) {
5684 5661 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5685 5662 }
@@ -5693,9 +5670,8 @@
5693 5670 exit();
5694 5671 }
5695 5672
5696 5673 // We are done, but in failure. Don't poll.
5697 - $content = "";
5698 5674 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5699 5675
5700 5676 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5701 5677 // For failed it shows a "Retry payment" button that takes the customer to /pay-for-order where it will be retried. LP 2026-03-17
@@ -5703,8 +5679,9 @@
5703 5679 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5704 5680 wp_redirect($failure_redirect);
5705 5681 exit();
5706 5682 }
5683 +
5707 5684 if ($status == 'cancelled' || $payment == 'cancelled') {
5708 5685 $this->maybe_restore_cart($orderid,'failed');
5709 5686 if ($failure_redirect){
5710 5687 wp_redirect($failure_redirect);
@@ -5709,8 +5686,38 @@
5709 5686 if ($failure_redirect){
5710 5687 wp_redirect($failure_redirect);
5711 5688 exit();
5712 5689 }
5690 + } else {
5691 + // If not, enqueue the status checker IOK 2026-09-21
5692 + wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5693 + }
5694 +
5695 + $this->log(sprintf(__("Order status of %1\$d not ready in order return: payment status %2\$s", 'woo-vipps'), $orderid, $payment), 'info');
5696 +
5697 + // Communicate this to the shortcode IOK 2026-09-21
5698 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5699 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5700 + });
5701 +
5702 + }
5703 +
5704 + public function vipps_wait_for_payment() {
5705 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5706 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5707 +
5708 + $orderid = $data['orderid'] ?? 0;
5709 + $status = $data['status'] ?? "";
5710 + $payment = $data['payment'] ?? "";
5711 +
5712 + $order = wc_get_order($orderid);
5713 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5714 +
5715 + do_action('woo_vipps_wait_for_payment_page',$order);
5716 + $gw = $this->gateway();
5717 +
5718 + $content = "";
5719 + if ($status == 'cancelled' || $payment == 'cancelled') {
5713 5720 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5714 5721 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5715 5722 $content .= "</div>";
5716 5723 return $this->special_page_html('', $content);
@@ -5716,12 +5723,9 @@
5716 5723 return $this->special_page_html('', $content);
5717 5724 }
5718 5725
5719 5726 // Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27
5720 -
5721 5727 // 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 5728 $signal = $this->callbackSignal($order);
5725 5729 $content = "";
5726 5730 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5727 5731
@@ -5729,17 +5733,17 @@
5729 5733 $signalurl = $this->callbackSignalURL($signal);
5730 5734
5731 5735 $content .= "</p></div>";
5732 5736
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) . "'>";
5737 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5738 +
5739 + // Carry the order status to the checking script IOK 2026-09-21
5740 + $content .= "<form id='vippsdata'>";
5736 5741 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5737 5742 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5738 5743 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5739 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5744 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5740 5745 $content .= "</form>";
5741 -
5742 5746
5743 5747 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5744 5748 $content .= "<p>" . __('An error occured during order confirmation. The error has been logged. Please contact us to determine the status of your order', 'woo-vipps') . "</p>";
5745 5749 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';