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