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 +881 -749 6.1.3 → 6.3.0 View file →
@@ -70,9 +70,9 @@
70 70 public static function CheckoutName($order=null) {
71 71 return "Vipps MobilePay Checkout"; // Do not translate
72 72 }
73 73 public static function ExpressCheckoutName($order=null) {
74 - return __("Vipps Express Checkout", 'woo-vipps');
74 + return __("Vipps MobilePay Express Checkout", 'woo-vipps');
75 75 }
76 76 public static function LoginName() {
77 77 return __("Login with Vipps", 'woo-vipps');
78 78 }
@@ -117,16 +117,22 @@
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 - // Express Checkout and Vipps Checkout supports the new pickup_location shipping method, but the admin interface for this may
126 + // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
126 127 // not have loaded if the default checkout solution isn't the Checkout block. We'll load it anyway if the user has any local pickup locations
127 128 // stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25
128 129 add_action('woocommerce_load_shipping_methods', array($Vipps, 'maybe_load_pickup_locations'), 90);
130 +
131 + // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
132 + // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
133 + // is important.
134 + add_filter('woocommerce_create_pages', array($Vipps, 'woocommerce_create_pages'), 50, 1);
129 135 }
130 136
131 137 // Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10
132 138 public function woocommerce_blocks_loaded() {
@@ -222,8 +228,9 @@
222 228 // Register certain scripts in wp_loaded because they will be added to the backend as well - the gutenberg checkout block
223 229 // needs these to be defined in the backend. IOK 2024-04-16
224 230 add_action('wp_loaded', array($this, 'wp_register_scripts'));
225 231 add_action('wp_enqueue_scripts', array($this, 'wp_enqueue_scripts'));
232 + add_action('wp_enqueue_scripts', array($this, 'enqueue_classic_checkout_scripts'), 20);
226 233
227 234 // Remove the possibility of restarting failed orders etc. This will be fixed in the future. IOK 2023-05-26
228 235 add_filter('woocommerce_my_account_my_orders_actions', array($this,'woocommerce_my_account_my_orders_actions'), 10, 2);
229 236
@@ -239,16 +246,8 @@
239 246
240 247 // Extra order actions on the order screen, now using ajax to be compatible with HPOS. IOK 2022-12-02
241 248 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
242 249
243 - // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
244 - add_action('rest_api_init', function() {
245 - register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
246 - 'methods' => 'GET',
247 - 'callback' => [$this, 'rest_express_checkout_products'],
248 - 'permission_callback' => '__return_true',
249 - ]);
250 - });
251 250
252 251 // We need a 5-minute scheduled event for the handler for missed callbacks. Using the
253 252 // action scheduler would be better, but we can't do that just yet because of backwards
254 253 // compatibility. At some point, support for older woo-versions should be dropped; then this
@@ -280,10 +279,63 @@
280 279 }
281 280
282 281 // Set default button options, migrating any older setup IOK 2026-07-15
283 282 $this->init_button_options();
283 +
284 + /*
285 + From version 6.2.x we create a real physical page to handle the "special" vipps pages,
286 + where we earlier used just a fake page with no real page id, unless especially configured.
287 + We therefore need to add code to maintain this special page.
288 +
289 + woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
290 + and we hook unto this with woocommerce_create_pages. LP 2026-09-03
291 + */
292 +
293 + // Delete special page id option when its deleted or trashed, so that we dont have to load
294 + // in the post to check status in woocommerce_loaded when we ensure the special page exists. LP 2026-09-03
295 + $delete_special_page_id = function($post_id, $post = null) {
296 + if (static::get_special_page_id() === $post_id) {
297 + delete_option('woocommerce_vipps_special_page_page_id');
298 + }
299 + };
300 + add_action('delete_post', $delete_special_page_id, 10, 2);
301 + add_action('wp_trash_post', $delete_special_page_id, 10, 2);
302 +
303 + $this->ensure_special_page_exists();
304 +
305 +
306 + // We want this special page to have a certain title and maybe special scripts and so on,
307 + // this gets run in template redirect for these pages.
308 + add_action('woo_vipps_before_handling_special_page', array($this, 'pre_special_page_actions'));
309 +
310 + // Add an admin interface for this page as well IOK 2026-09-11
311 + add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
284 312 }
285 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 + ]);
336 + }
337 +
286 338 public function admin_init () {
287 339 $gw = $this->gateway();
288 340 require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php");
289 341 $adminSettings = VippsAdminSettings::instance();
@@ -310,10 +362,8 @@
310 362 // Styling etc
311 363 add_action('admin_head', array($this, 'admin_head'));
312 364
313 365 // Scripts
314 - $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
315 - wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
316 366 add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts'));
317 367
318 368 // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
319 369 add_action('current_screen', function ($screen) {
@@ -401,9 +451,77 @@
401 451 }
402 452 }
403 453 }
404 454
455 +
456 + /** Ensure we have a special page for payment flows
457 + *
458 + * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
459 + * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
460 + **/
461 + public function ensure_special_page_exists() {
462 + if (static::get_special_page_id()) return;
463 + $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
405 464
465 + // If user had in previous version overriden the fake page with a real one: migrate this page to be the special page. LP 2026-09-01
466 + $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
467 + if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
468 + $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
469 + // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
470 + update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
471 +
472 + // Ensure this page has the necessary shortcode. LP 2026-09-01
473 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
474 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
475 + wp_update_post([
476 + 'ID' => $old_special_page_id,
477 + 'post_content' => $new_content,
478 + ]);
479 + }
480 + } else {
481 + // Create special page if its missing. LP 2026-09-01
482 + $this->maybe_create_vipps_pages();
483 + }
484 + }
485 +
486 + // Admin interface for the special page on woo/advanced/pages
487 + public function woocommerce_settings_pages ($settings) {
488 + $i = -1;
489 + foreach($settings as $entry) {
490 + $i++;
491 + if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
492 + break;
493 + }
494 + }
495 + if ($i > 0) {
496 + $vippspagesettings = array(
497 + array(
498 + 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
499 + 'desc' => sprintf(__('This page is used for various special pages used by %1$s', 'woo-vipps'), Vipps::CompanyName()) . sprintf( __( 'Page contents: [%1$s]', 'woocommerce' ), 'vipps_special_page') ,
500 + 'id' => 'woocommerce_vipps_special_page_page_id',
501 + 'type' => 'single_select_page_with_search',
502 + 'default' => '',
503 + 'class' => 'wc-page-search',
504 + 'css' => 'min-width:300px;',
505 + 'args' => array(
506 + 'exclude' =>
507 + array(
508 + wc_get_page_id( 'myaccount' ),
509 + wc_get_page_id( 'checkout' ),
510 + wc_get_page_id( 'cart' ),
511 + ),
512 + ),
513 + 'desc_tip' => true,
514 + 'autoload' => false,
515 + ));
516 + array_splice($settings, $i, 0, $vippspagesettings);
517 + }
518 +
519 + return $settings;
520 + }
521 +
522 +
523 +
406 524 // Runs on init, adds the Vipps badge feature if activated
407 525 public function maybe_add_vipps_badge_feature () {
408 526 $badge_options = get_option('vipps_badge_options');
409 527 if (!$badge_options || !@$badge_options['badgeon']) return false;
@@ -732,8 +850,11 @@
732 850
733 851 // Get current brand and language
734 852 $current_brand = strtolower($this->get_payment_method_name());
735 853 $current_language = $this->get_customer_language();
854 + if ('se' === $current_language) $current_language = 'sv';
855 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
856 + if ('dk' === $current_language) $current_language = 'da';
736 857
737 858 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
738 859 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
739 860 'purple'=> __('Purple', 'woo-vipps')];
@@ -937,12 +1058,30 @@
937 1058
938 1059 public function get_html_button_attrs_for_context($context = 'global') {
939 1060 $options = get_option('vipps_button_options2', []);
940 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 +
941 1074 $config = $options['express']['configs'][$context] ?? [];
942 - if (!$config || ($config['use-global-config'] ?? false)) {
1075 + $use_global = !$config || ($config['use-global-config'] ?? false);
1076 + if ($use_global) {
943 1077 $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
944 1078 }
1079 +
1080 + // see above.
1081 + if ($gutenberg) {
1082 + $config['stretched']='true';
1083 + }
945 1084 return $config;
946 1085 }
947 1086
948 1087 public function get_html_button_for_context($context = 'global') {
@@ -1029,8 +1168,10 @@
1029 1168 private function button_menu_express_section() {
1030 1169 $options = get_option('vipps_button_options2', []);
1031 1170 $express = $options['express'] ?? [];
1032 1171 $configs = $express['configs'] ?? [];
1172 +
1173 +
1033 1174 $contexts = [
1034 1175 'global' => __('Global', 'woo-vipps'),
1035 1176 'product' => __('Product', 'woo-vipps'),
1036 1177 'catalog' => __('Catalog', 'woo-vipps'),
@@ -1161,9 +1302,9 @@
1161 1302
1162 1303 // Update the preview web component's attributes. LP 2026-06-24
1163 1304 function updatePreview(event) {
1164 1305 const args = getPreviewArgs();
1165 - // LP FIXME: when i use get_customer_language() here it gives me my user language, but on frontend it gives the site language, i.e not the same value. So this preview will be wrong language. so use get_locale for now. LP 2026-07-02
1306 + // FIXME: when i use get_customer_language() here it gives me my user language, but on frontend it gives the site language, i.e not the same value. So this preview will be wrong language. so use get_locale for now. LP 2026-07-02
1166 1307 // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1167 1308 if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1168 1309 const button = jQuery('#vipps-button-express-preview');
1169 1310 button.attr(args);
@@ -1203,8 +1344,9 @@
1203 1344
1204 1345 // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1205 1346 const newContext = jQuery("#context").val();
1206 1347 const newConfig = contextConfigs[newContext];
1348 +
1207 1349 setInputsFromConfig(newContext, newConfig);
1208 1350 currentContext = newContext;
1209 1351 }
1210 1352
@@ -1567,12 +1709,14 @@
1567 1709 <?php
1568 1710 }
1569 1711 // Scripts used in the backend
1570 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);
1571 1717 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1572 - $this->script_add_vippslocale();
1573 -
1574 - 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');
1575 1719 wp_enqueue_script('vipps-admin');
1576 1720
1577 1721 wp_enqueue_style('vipps-admin-style',plugins_url('css/admin.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/admin.css"), 'all');
1578 1722 wp_enqueue_style('vipps-fonts');
@@ -1642,12 +1786,9 @@
1642 1786
1643 1787 public function wp_register_scripts () {
1644 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
1645 1789 // (which it only is if Gutenberg is active) or not provided at all, add it now.
1646 - if (!wp_script_is( 'wp-hooks', 'registered')) {
1647 - wp_register_script('wp-hooks', plugins_url('/compat/hooks.min.js', __FILE__));
1648 - }
1649 - 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);
1650 1791
1651 1792 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1652 1793 wp_register_script('vipps-onsite-messageing',
1653 1794 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
@@ -1653,11 +1794,25 @@
1653 1794 plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1654 1795 array(),
1655 1796 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1656 1797 [
1657 - 'in_footer' => true,
1658 - 'strategy' => 'async',
1798 + 'in_footer' => true,
1799 + 'strategy' => 'async',
1659 1800 ],
1801 + );
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]
1660 1815 );
1661 1816
1662 1817 // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1663 1818 wp_register_script('vipps-button-webcomponent',
@@ -1664,29 +1819,46 @@
1664 1819 plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1665 1820 array(),
1666 1821 filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1667 1822 [
1668 - 'in_footer' => true,
1669 - 'strategy' => 'async',
1670 - ],
1823 + 'in_footer' => false
1824 + ]
1671 1825 );
1672 1826 }
1673 1827
1674 1828 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1675 - public function script_add_vippslocale () {
1829 + public function script_add_vippslocale ($handle) {
1676 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();
1677 1832 $strings = array(
1678 - 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1679 - 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()),
1680 - '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'),
1681 1851 );
1682 - wp_localize_script('vipps-gw', 'VippsLocale', $strings);
1852 + wp_localize_script($handle, 'VippsLocale', $strings);
1683 1853 }
1684 1854
1685 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');
1686 1858 wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
1687 1859 // Add certain translations very late so translation plugins get a chance to work. IOK 2026-02-02
1688 - $this->script_add_vippslocale();
1860 + $this->script_add_vippslocale('vipps-gw');
1689 1861
1690 1862 wp_enqueue_script('vipps-gw');
1691 1863 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1692 1864 wp_enqueue_script('vipps-button-webcomponent');
@@ -1691,13 +1863,55 @@
1691 1863 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1692 1864 wp_enqueue_script('vipps-button-webcomponent');
1693 1865 }
1694 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' ) );
1695 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 +
1696 1911 public function add_shortcodes() {
1697 1912 add_shortcode('woo_vipps_buy_now', array($this, 'buy_now_button_shortcode'));
1698 1913 add_shortcode('woo_vipps_express_checkout_button', array($this, 'express_checkout_button_shortcode'));
1699 - add_shortcode('woo_vipps_express_checkout_banner', array($this, 'express_checkout_banner_shortcode'));
1700 1914
1701 1915 // Badges, if using shortcodes
1702 1916 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1703 1917 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
@@ -1702,8 +1916,11 @@
1702 1916 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1703 1917 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1704 1918 // Legacy vipps-badge shortcode. LP 19.11.2024
1705 1919 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1920 +
1921 + // special page handling, previously a fake page. LP 2026-08-25
1922 + add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1706 1923 }
1707 1924
1708 1925
1709 1926 public function log ($what,$type='info') {
@@ -1729,8 +1946,10 @@
1729 1946 }
1730 1947
1731 1948 // Show express button option on checkout form. LP 2026-03-23
1732 1949 public function checkout_before_customer_details_express () {
1950 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1951 + do_action('woo_vipps_checkout_before_customer_details_express');
1733 1952 $gw = $this->gateway();
1734 1953 if (!$gw->show_express_checkout()) return;
1735 1954 $this->express_checkout_section_html();
1736 1955 }
@@ -1744,77 +1963,51 @@
1744 1963 $this->checkout_express_checkout_button_html();
1745 1964 echo '</fieldset>';
1746 1965 }
1747 1966
1748 - public function express_checkout_banner() {
1967 + // Show the express button if reasonable to do so
1968 + public function cart_express_checkout_button() {
1749 1969 $gw = $this->gateway();
1750 - if (!$gw->show_express_checkout()) return;
1751 - return $this->express_checkout_banner_html();
1752 - }
1753 1970
1754 - public function express_checkout_banner_html() {
1755 - $url = $this->express_checkout_url();
1756 - $url = wp_nonce_url($url,'express','sec');
1757 - $text = __('Skip entering your address and just checkout using', 'woo-vipps');
1758 - $linktext = 'Express'; // dont translate. LP 2025-09-03
1759 - $logo = $this->get_express_banner_logo();
1760 - $payment_method = $this->get_payment_method_name();
1761 -
1762 - $img_classes = 'express-banner-logo inline negative ' . strtolower($payment_method) . '-logo';
1763 - $div_classes = 'woocommerce-info ' . strtolower($payment_method) . '-info';
1764 - $a_classes = 'express-banner-link ' . strtolower($payment_method) . '-link';
1765 -
1766 - $message = $text . "<a href='$url' class='$a_classes'><img class='$img_classes' border=0 src='$logo' alt='$payment_method'/>$linktext!</a>";
1767 - $message = apply_filters('woo_vipps_express_checkout_banner', $message, $url, $payment_method);
1768 - ?>
1769 - <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1770 - <?php
1971 + if ($gw->show_express_checkout()){
1972 + return $this->cart_express_checkout_button_html();
1973 + }
1771 1974 }
1772 1975
1773 - public function checkout_express_checkout_button() {
1976 + public function minicart_express_checkout_button() {
1774 1977 $gw = $this->gateway();
1775 1978
1776 1979 if ($gw->show_express_checkout()){
1777 - return $this->checkout_express_checkout_button_html();
1980 + return $this->cart_express_checkout_button_html('minicart');
1778 1981 }
1779 1982 }
1780 1983
1781 - public function checkout_express_checkout_button_html() {
1782 - $url = $this->express_checkout_url();
1783 - $url = wp_nonce_url($url,'express','sec');
1784 - $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));
1785 1994 $method = $this->get_payment_method_name();
1786 1995 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1787 - $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>";
1788 1999 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1789 2000 echo $html;
1790 2001 }
1791 2002
1792 - // Show the express button if reasonable to do so
1793 - public function cart_express_checkout_button() {
1794 - $gw = $this->gateway();
1795 -
1796 - if ($gw->show_express_checkout()){
1797 - return $this->cart_express_checkout_button_html();
1798 - }
1799 - }
1800 -
1801 - public function minicart_express_checkout_button() {
1802 - $gw = $this->gateway();
1803 -
1804 - if ($gw->show_express_checkout()){
1805 - return $this->cart_express_checkout_button_html(true);
1806 - }
1807 - }
1808 -
1809 - public function cart_express_checkout_button_html($minicart = false) {
1810 - $url = $this->express_checkout_url();
1811 - $url = wp_nonce_url($url,'express','sec');
1812 - $context = $minicart ? 'minicart' : 'cart';
1813 - $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'));
1814 2005 $method = $this->get_payment_method_name();
1815 2006 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1816 - $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>";
1817 2010 $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1818 2011 echo $html;
1819 2012 }
1820 2013
@@ -1853,19 +2046,11 @@
1853 2046 public function express_checkout_button_shortcode() {
1854 2047 $gw = $this->gateway();
1855 2048 if (!$gw->cart_supports_express_checkout()) return;
1856 2049 ob_start();
1857 - $this->cart_express_checkout_button_html('shortcode');
2050 + $this->cart_express_checkout_button_html('cart');
1858 2051 return ob_get_clean();
1859 2052 }
1860 - // Show a banner normally shown for non-logged-in-users at the checkout page. It does not need to check if we are to show the button, obviously, but needs to see if the cart works
1861 - public function express_checkout_banner_shortcode() {
1862 - $gw = $this->gateway();
1863 - if (!$gw->cart_supports_express_checkout()) return;
1864 - ob_start();
1865 - $this->express_checkout_banner_html();
1866 - return ob_get_clean();
1867 - }
1868 2053
1869 2054 // Manage the various product meta fields
1870 2055 public function process_product_meta ($id, $post) {
1871 2056 // This is for the 'buy now' button
@@ -2516,77 +2701,93 @@
2516 2701 return null;
2517 2702 }
2518 2703 }
2519 2704
2705 + // Special pages, and some callbacks. IOK 2018-05-18
2706 + public function template_redirect() {
2520 2707
2521 - // If this is a special page, return true very early because we are handling this. IOK 2023-02-22
2522 - public function pre_handle_404($current, $query) {
2523 - if (!is_admin()) {
2524 - $special = $this->is_special_page();
2525 - if ($special) {
2526 - // Ensure very early on that Autooptimize does not try to optimize us (if installed) IOK 2023-03-04
2527 - add_filter( 'autoptimize_filter_noptimize', '__return_true');
2528 - return true;
2529 - }
2708 + // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2709 + // We redirect these to the new location.
2710 + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2711 + if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2712 + $url = static::get_special_page_url();
2713 + $_GET['action'] = 'buy_product';
2714 + $q = build_query($_GET);
2715 + wp_redirect($url . "?" . $q, 302);
2716 + exit();
2530 2717 }
2531 - return $current;
2718 +
2719 + if (static::is_special_page()) {
2720 + // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2721 + remove_filter('template_redirect', 'redirect_canonical', 10);
2722 + // dont cache special page. LP 2026-08-25
2723 + $this->nocache();
2724 + // Do the custom pre-load actions for these pages IOK 2026-09-11
2725 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2726 + }
2532 2727 }
2533 2728
2534 - // Special pages, and some callbacks. IOK 2018-05-18
2535 - public function template_redirect() {
2536 - global $post;
2537 - // Handle special callbacks
2538 - $special = $this->is_special_page() ;
2729 + // Ran in template redirect for the special page. IOK 2026-09-2
2730 + public function pre_special_page_actions ($action) {
2731 + // Change title dynamically depending on action. LP 2026-09-02
2732 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2539 2733
2540 - if ($special) {
2541 - remove_filter('template_redirect', 'redirect_canonical', 10);
2542 - do_action('woo_vipps_before_handling_special_page', $special);
2734 + // If we are handling the 'wait for payment' action, we need to poll the order status before
2735 + // we start producing content IOK 2026-09-21
2736 + if ($action == 'wait_for_payment') {
2737 + $this->handle_payment_poll_and_redirect();
2738 + }
2543 2739
2544 - // Allow above hook to actually handle special pages. It should probably call $Vipps->fakepage or a redirect; can be used
2545 - // to intercept express checkout etc. IOK 2022-03-18
2546 - if (! apply_filters('woo_vipps_special_page_handled', false, $special)) {
2547 - $this->$special();
2548 - }
2740 + // Some validation is required for this action
2741 + if ($action == 'do_express_checkout') {
2742 + $this->vipps_express_checkout_consistency_check();
2549 2743 }
2744 + // These two actions require an extra script
2745 + if (in_array($action, ['buy_product','do_express_checkout'])) {
2746 + wp_enqueue_script('vipps-purchase', plugins_url('js/vipps-purchase.js',__FILE__), ['vipps-gw'],
2747 + filemtime(dirname(__FILE__) . "/js/vipps-purchase.js"),
2748 + ['in_footer'=>true]
2749 + );
2750 + }
2751 + }
2550 2752
2551 - $consentremoval = $this->is_consent_removal();
2552 - if ($consentremoval) {
2553 - remove_filter('template_redirect', 'redirect_canonical', 10);
2554 - do_action('woo_vipps_before_handling_special_page', 'consentremoval');
2555 - if (! apply_filters('woo_vipps_special_page_handled', false, 'consentremoval')) {
2556 - $this->vipps_consent_removal_callback($consentremoval);
2753 + // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2754 + public function vipps_special_page_endpoint_title($title, $postid = 0) {
2755 + global $wp_query;
2756 + // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2757 +
2758 + // In block themes the whole template (header, footer, content) renders inside the main
2759 + // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2760 + // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2761 + // the queried page so an earlier title doesn't consume this one-shot filter.
2762 + if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2763 + switch ($_GET['action'] ?? '') {
2764 + case 'wait_for_payment':
2765 + $title = __('Processing order', 'woo-vipps');
2766 + break;
2767 + case 'do_express_checkout':
2768 + case 'buy_product':
2769 + $title = __('Express Checkout', 'woo-vipps');
2770 + break;
2557 2771 }
2558 2772 }
2773 + return $title;
2559 2774 }
2775 +
2560 2776 // Template handling for special pages. IOK 2018-11-21
2777 + // This is legacy - the special page is now a real page, so it can have a special template using standard WP methods. IOK 2026-09-11
2561 2778 public function template_include($template) {
2562 - $special = $this->is_special_page() ;
2563 - if ($special) {
2779 + if (static::is_special_page()) {
2564 2780 // Get any special template override from the options IOK 2020-02-18
2565 2781 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2566 2782 $found = locate_template($specific,false,false);
2567 2783 if ($found) $template=$found;
2568 2784
2569 - return apply_filters('woo_vipps_special_page_template', $template, $special);
2785 + return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2570 2786 }
2571 2787 return $template;
2572 2788 }
2573 2789
2574 -
2575 - // Can't use wc-api for this, as that does not support DELETE . IOK 2018-05-18
2576 - private function is_consent_removal () {
2577 -
2578 - if ($_SERVER['REQUEST_METHOD'] != 'DELETE') return false;
2579 - if ( !get_option('permalink_structure')) {
2580 - if (@$_REQUEST['vipps-consent-removal']) return @$_REQUEST['callback'];
2581 - return false;
2582 - }
2583 - if (preg_match("!/vipps-consent-removal/([^/]*)!", $_SERVER['REQUEST_URI'], $matches)) {
2584 - return @$_REQUEST['callback'];
2585 - }
2586 - return false;
2587 - }
2588 -
2589 2790 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2590 2791 // the user if using Express Checkout IOK 2020-10-09
2591 2792 public function woocommerce_before_thankyou ($orderid) {
2592 2793 $order = wc_get_order($orderid);
@@ -2591,9 +2792,9 @@
2591 2792 public function woocommerce_before_thankyou ($orderid) {
2592 2793 $order = wc_get_order($orderid);
2593 2794 if ($order) {
2594 2795 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2595 - // -- or the same thing for Vipps Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2796 + // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2596 2797 $this->maybe_log_in_user($order);
2597 2798 $order->delete_meta_data('_vipps_limited_session');
2598 2799 $order->save();
2599 2800
@@ -2654,9 +2855,8 @@
2654 2855
2655 2856 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2656 2857 // IOK 2025-11-19
2657 2858 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2658 -
2659 2859 }
2660 2860
2661 2861 public function get_payment_method_name() {
2662 2862 return $this->gateway()->get_option('payment_method_name');
@@ -2676,14 +2876,13 @@
2676 2876 public function after_setup_theme() {
2677 2877 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2678 2878 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2679 2879
2680 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2880 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2681 2881 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2682 2882 // is important.
2683 2883 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2684 2884
2685 -
2686 2885 // Callbacks use the Woo API IOK 2018-05-18
2687 2886 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2688 2887 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2689 2888
@@ -2694,19 +2893,21 @@
2694 2893 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2695 2894 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2696 2895
2697 2896 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2698 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2897 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2699 2898 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2700 2899
2701 2900 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2702 2901 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2703 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'));
2704 2907
2705 - // Special pages and callbacks handled by template_redirect
2706 - // We must also notify WP and other plugins that we are handling this 404-like situation. IOK 2023-02-22
2908 + // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2707 2909 add_action('template_redirect', array($this,'template_redirect'),1);
2708 - add_action('pre_handle_404', array($this, 'pre_handle_404'), 1, 2);
2709 2910
2710 2911 // Allow overriding their templates
2711 2912 add_filter('template_include', array($this,'template_include'), 10, 1);
2712 2913
@@ -2713,21 +2914,8 @@
2713 2914 // Ajax endpoints for checking the order status while waiting for confirmation
2714 2915 add_action('wp_ajax_nopriv_check_order_status', array($this, 'ajax_check_order_status'));
2715 2916 add_action('wp_ajax_check_order_status', array($this, 'ajax_check_order_status'));
2716 2917
2717 -
2718 - // Buying a single product directly using express checkout IOK 2018-09-28
2719 - add_action('wp_ajax_nopriv_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2720 - add_action('wp_ajax_vipps_buy_single_product', array($this, 'ajax_vipps_buy_single_product'));
2721 -
2722 - // This is for express checkout which we will also do asynchronously IOK 2018-05-28
2723 - add_action('wp_ajax_nopriv_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2724 - add_action('wp_ajax_do_express_checkout', array($this, 'ajax_do_express_checkout'));
2725 -
2726 - // Same thing, but for single products IOK 2018-05-28
2727 - add_action('wp_ajax_nopriv_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2728 - add_action('wp_ajax_do_single_product_express_checkout', array($this, 'ajax_do_single_product_express_checkout'));
2729 -
2730 2918 // Handle the cancel unpaid order action when the "hold stock" times out.
2731 2919 // For *normal* vipps orders, we run another cronjob every 5. minute which checks order status,
2732 2920 // therefore here it suffices to check if the order is 'cancelled' at Vipps, and if so we return.
2733 2921 // For Checkout the rules are different though.
@@ -2806,9 +2994,8 @@
2806 2994 $this->vippsJSConfig = array();
2807 2995 $this->vippsJSConfig['vippsajaxurl'] = admin_url('admin-ajax.php');
2808 2996 $this->vippsJSConfig['BuyNowWith'] = __('Buy now with', 'woo-vipps');
2809 2997 $this->vippsJSConfig['BuyNowWithVipps'] = sprintf(__('Buy now with %1$s', 'woo-vipps'), $this->get_payment_method_name());
2810 - $this->vippsJSConfig['vippslogourl'] = plugins_url('img/vipps_logo_negativ_rgb_transparent.png',__FILE__);
2811 2998 $this->vippsJSConfig['vippssmileurl'] = plugins_url('img/vmp-logo.png',__FILE__);
2812 2999 $this->vippsJSConfig['vippsbuynowbutton'] = sprintf(__( '%1$s Buy Now button', 'woo-vipps' ), $this->get_payment_method_name());
2813 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());
2814 3001 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
@@ -2813,8 +3000,10 @@
2813 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());
2814 3001 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2815 3002 $this->vippsJSConfig['vippslocale'] = get_locale();
2816 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();
2817 3006
2818 3007
2819 3008 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2820 3009 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
@@ -2992,14 +3181,14 @@
2992 3181
2993 3182 $raw_post = @file_get_contents( 'php://input' );
2994 3183 $result = @json_decode($raw_post,true);
2995 3184
2996 - // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
3185 + // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
2997 3186 // .. and the epayment webhooks 2023-12-19
2998 3187 $ischeckout = false;
2999 3188 $iswebhook = false;
3000 3189 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
3001 - // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
3190 + // For Checkout v3 and onwards, we control the callback so the type is just this field
3002 3191 if ($callback == 'checkout') {
3003 3192 $ischeckout = true;
3004 3193 }
3005 3194 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
@@ -3413,10 +3602,10 @@
3413 3602 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3414 3603 exit();
3415 3604 }
3416 3605
3417 - // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3418 - // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3606 + // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3607 + // special support for Checkout that this is in fact happening. IOK 2023-01-19
3419 3608 // This needs to be done before "calculate totals".
3420 3609 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3421 3610 $ischeckout = $order->get_meta('_vipps_checkout');
3422 3611
@@ -3592,9 +3781,9 @@
3592 3781 ), 'debug');
3593 3782
3594 3783 }
3595 3784
3596 - // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3785 + // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3597 3786 // 'dynamic pricing' 2023-01-26
3598 3787 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3599 3788 $taxrate = 0;
3600 3789 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
@@ -3720,9 +3909,9 @@
3720 3909 $vippsmethod['shippingMethod'] = $rate->get_label();
3721 3910 $vippsmethod['shippingMethodId'] = $key;
3722 3911 $vippsmethods[]=$vippsmethod;
3723 3912
3724 - // Metadata and settings stored for later use for Vipps Checkout
3913 + // Metadata and settings stored for later use for Checkout
3725 3914 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3726 3915 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3727 3916 // finalize the order. IOK 2025-08-15
3728 3917 $ratemap[$key]=$rate;
@@ -3740,9 +3929,9 @@
3740 3929 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3741 3930 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3742 3931 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3743 3932
3744 - // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3933 + // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3745 3934 // IOK 2025-08-15 and new Express yet another slightly different format.
3746 3935 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3747 3936 if ($ischeckout) {
3748 3937 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
@@ -4032,9 +4221,9 @@
4032 4221 WC()->cart->calculate_totals();
4033 4222 WC()->cart->set_session();
4034 4223 return true;
4035 4224 } catch (Exception $e) {
4036 - $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');
4037 4226 return false;
4038 4227 }
4039 4228 }
4040 4229
@@ -4118,17 +4307,8 @@
4118 4307 header("X-Accel-Expires: 0");
4119 4308 }
4120 4309
4121 4310
4122 -
4123 - // Handle DELETE on a vipps consent removal callback
4124 - public function vipps_consent_removal_callback ($callback) {
4125 - Vipps::nocache();
4126 - // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
4127 - // to escape here when the API is changed. IOK 2020-10-14
4128 - $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
4129 - }
4130 -
4131 4311 public function woocommerce_payment_gateways($methods) {
4132 4312 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4133 4313 require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
4134 4314 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
@@ -4153,9 +4333,11 @@
4153 4333 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
4154 4334 return $url;
4155 4335 }
4156 4336 $url = $this->express_checkout_url();
4157 - $url = wp_nonce_url($url,'express','sec');
4337 + // At this point, there is always a query argument here. IOK 2026-09-21
4338 + $nonce = wp_create_nonce('express');
4339 + $url = $url . "&sec=$nonce";
4158 4340
4159 4341 return $url;
4160 4342 }
4161 4343
@@ -4222,9 +4404,9 @@
4222 4404 // Poll status and correct woo status. LP 2026-05-19
4223 4405 $order_data = $gw->get_payment_details($order);
4224 4406
4225 4407 // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4226 - if ('CANCEL' === $order_data['STATE']) {
4408 + if ('CANCEL' === ($order_data['state'] ?? "")) {
4227 4409 /* translators: company name */
4228 4410 $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4229 4411 return;
4230 4412 }
@@ -4251,20 +4433,40 @@
4251 4433 }
4252 4434 }
4253 4435
4254 4436 public function activate () {
4255 - static::maybe_add_cron_event();
4256 - $gw = $this->gateway();
4437 + static::maybe_add_cron_event();
4438 + $gw = $this->gateway();
4257 4439
4258 - // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4259 - if ($gw->get_option('orderprefix') == 'Woo') {
4260 - $gw->update_option('orderprefix', $this->generate_order_prefix());
4261 - }
4262 - // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4263 - $gw->initialize_webhooks();
4264 - $this->payment_method_name = $gw->get_option('payment_method_name');
4265 - }
4440 + // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4441 + if ($gw->get_option('orderprefix') == 'Woo') {
4442 + $gw->update_option('orderprefix', $this->generate_order_prefix());
4443 + }
4444 + // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4445 + $gw->initialize_webhooks();
4446 + $this->payment_method_name = $gw->get_option('payment_method_name');
4266 4447
4448 +
4449 + // Check if the special page is noted and actually does exist
4450 + $special = static::get_special_page_id();
4451 + if ($special) {
4452 + $special_page = get_post($special);
4453 + if ($special_page && 'trash' !== $special_page->post_status) {
4454 + // Ensure this page has the necessary shortcode. LP 2026-09-01
4455 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4456 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4457 + wp_update_post([
4458 + 'ID' => $special,
4459 + 'post_content' => $new_content,
4460 + ]);
4461 + }
4462 + } else {
4463 + delete_option('woocommerce_vipps_special_page_page_id');
4464 + }
4465 + }
4466 +
4467 + }
4468 +
4267 4469 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4268 4470 public static function deactivate() {
4269 4471 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4270 4472 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
@@ -4317,9 +4519,10 @@
4317 4519 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4318 4520 private function maybe_set_vipps_as_default() {
4319 4521 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4320 4522 $gw = $this->gateway();
4321 - if ($gw->get_option('vippsdefault')=='yes') {
4523 + // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4524 + if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4322 4525 WC()->session->set('chosen_payment_method', $gw->id);
4323 4526 }
4324 4527 }
4325 4528
@@ -4331,28 +4534,12 @@
4331 4534 $order = wc_get_order($order->get_id());
4332 4535 $order_status = $order->get_status();
4333 4536
4334 4537 if ($order_status != 'pending') return $order_status;
4335 - // No callback has occured yet. If this has been going on for a while, check directly with Vipps
4336 - // We can't use the vipps init timestamp here, because that may be in the past for Checkout at least. IOK 2025-08-13
4337 - if ($order_status == 'pending') {
4338 - if (WC()->session) {
4339 - $now = time();
4340 - $then = WC()->session->get('_vipps_check_' . $order->get_id());
4341 - if (!$then) {
4342 - $then = $now;
4343 - WC()->session->set('_vipps_check_' . $order->get_id(), $then);
4344 - }
4345 - if (($then + (1 * 30)) > $now) { // more than half a minute? Start checking at Vipps
4346 - return $order_status;
4347 - }
4348 - } else {
4349 - // No session shouldn't be possible, but if it is..
4350 - return $order_status;
4351 - }
4352 - }
4538 +
4539 + $gw = $this->gateway();
4353 4540 $this->log("Checking order status on Vipps for order id: " . $order->get_id(), 'info');
4354 - return $this->check_status_of_pending_order($order);
4541 + $newstatus = $gw->poll_and_check_order_status($order);
4355 4542 }
4356 4543
4357 4544 // In some situations we have to empty the cart when the user goes to Vipps, so
4358 4545 // we store it in the session and restore it if the users cancels. IOK 2018-05-07
@@ -4420,17 +4607,18 @@
4420 4607
4421 4608 // Maybe log in user
4422 4609 // It is done on the thank-you page of the order, and only for express checkout.
4423 4610 function maybe_log_in_user ($order) {
4611 +
4424 4612 if (is_user_logged_in()) return;
4425 4613 if (!$order || ! self::is_vipps_order($order)) return;
4426 4614
4427 4615 // We *do* want to log in express checkout customers, but not those that
4428 - // use the Vipps Checkout solution - those can change their emails in the
4616 + // use the Checkout solution - those can change their emails in the
4429 4617 // checkout screen. IOK 2021-09-03
4430 4618 $do_login = $order->get_meta('_vipps_express_checkout');
4431 4619
4432 - // We will not log in Vipps Checkout users unless the option for that is true
4620 + // We will not log in Checkout users unless the option for that is true
4433 4621 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4434 4622 $do_login = false;
4435 4623 }
4436 4624
@@ -4465,9 +4653,9 @@
4465 4653
4466 4654 // Both Checkout and Express Checkout have the below value set to true
4467 4655 if (!$order->get_meta('_vipps_express_checkout')) return;
4468 4656
4469 - // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4657 + // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4470 4658 // IOK 2023-07-27
4471 4659 $ischeckout = $order->get_meta('_vipps_checkout');
4472 4660 if ($ischeckout) {
4473 4661 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
@@ -4567,129 +4755,233 @@
4567 4755 }
4568 4756 if (!$o) return;
4569 4757 if (!$o->get_meta('_vipps_single_product_express')) return;
4570 4758 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4571 - if ($failed) WC()->cart->empty_cart();
4759 + // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4760 + WC()->cart->empty_cart();
4572 4761 $this->restore_cart($o);
4573 4762 }
4574 4763
4575 4764
4576 - public function ajax_vipps_buy_single_product () {
4577 - Vipps::nocache();
4578 - static::set_locale_if_in_header();
4579 - // We're not checking ajax referer here, because what we do is creating a session and redirecting to the
4580 - // 'create order' page wherein we'll do the actual work. IOK 2018-09-28
4581 - $session = WC()->session;
4582 - if (!$session->has_session()) {
4583 - $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;
4584 4781 }
4585 - $session->set('__vipps_buy_product', json_encode($_REQUEST));
4586 4782
4587 - // Incredibly, some caches will cache this page even with cookies set and no-cache headers set. So we try to
4588 - // add yet another way to inform caches that this is, in fact, not cacheable. IOK 2023-06-12
4589 - $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 + }
4590 4791
4591 - $result = array('ok'=>1, 'msg'=>__('Processing order... ','woo-vipps'), 'url'=> $url);
4592 - wp_send_json($result);
4593 - 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;
4594 4800 }
4595 4801
4596 - public function ajax_do_express_checkout () {
4597 - check_ajax_referer('do_express','sec');
4598 - Vipps::nocache();
4599 - 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 +
4600 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);
4601 4848
4602 - if (!$gw->express_checkout_available() || !$gw->cart_supports_express_checkout()) {
4603 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4604 - wp_send_json($result);
4605 - 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;
4606 4857 }
4607 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 + }
4608 4866
4609 -
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 + }
4610 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 + }
4611 4906 // Validate cart going forward using same logic as WC_Cart->check_cart() but not adding notices.
4612 4907 $toolate = false;
4613 4908 $msg = "";
4614 4909 $valid = WC()->cart->check_cart_item_validity();
4615 4910 if ( is_wp_error( $valid) ) {
4616 - $toolate = true;
4617 - $msg = "<br>" . $valid->get_error_message();
4911 + $toolate = true;
4912 + $msg = "<br>" . $valid->get_error_message();
4618 4913 }
4619 4914 $stock = WC()->cart->check_cart_item_stock();
4620 - if ( is_wp_error( $stock) ) {
4621 - $toolate = true;
4622 - $msg = "<br>" . $stock->get_error_message();
4623 - }
4915 + if ( is_wp_error( $stock) ) {
4916 + $toolate = true;
4917 + $msg = "<br>" . $stock->get_error_message();
4918 + }
4624 4919
4625 4920 if ($toolate) {
4626 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);
4627 - wp_send_json($result);
4628 - exit();
4922 + return $result;
4629 4923 }
4630 4924
4631 - try {
4632 - $orderid = $gw->create_partial_order();
4633 - do_action('woo_vipps_ajax_do_express_checkout', $orderid);
4634 - } catch (Exception $e) {
4635 - $this->log($e->getMessage(),'error');
4636 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4637 - wp_send_json($result);
4638 - exit();
4639 - }
4640 - if (!$orderid) {
4641 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4642 - wp_send_json($result);
4643 - 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 + }
4644 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 + }
4645 4940
4646 - try {
4647 - $this->maybe_add_static_shipping($gw,$orderid);
4648 - } catch (Exception $e) {
4649 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4650 - $this->log($e->getMessage(),'error');
4651 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4652 - wp_send_json($result);
4653 - 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 + }
4654 4949 }
4655 -
4656 - $ok = $gw->process_payment($orderid);
4657 - if ($ok && $ok['result'] == 'success') {
4658 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4659 - wp_send_json($result);
4660 - 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();
4661 4956 }
4662 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4663 - wp_send_json($result);
4664 - exit();
4957 + return $result;
4958 +
4665 4959 }
4666 4960
4667 - // 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
4668 - public function ajax_do_single_product_express_checkout() {
4669 - check_ajax_referer('do_express','sec');
4670 - 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();
4671 4965 static::set_locale_if_in_header();
4672 - require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4673 - $gw = $this->gateway();
4674 -
4675 - if (!$gw->express_checkout_available()) {
4676 - $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4677 - wp_send_json($result);
4678 - 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]);
4679 4969 }
4970 + $result = ['ok' => 0, 'msg'=>'', 'orderid'=>0, 'url'=>''];
4680 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));
4681 4977
4682 - // 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
4683 - // 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
4684 - $varid = intval(@$_POST['variation_id']);
4685 - $prodid = intval(@$_POST['product_id']);
4686 - $sku = sanitize_text_field(@$_POST['sku']);
4687 - $quant = intval(@$_POST['quantity']);
4688 4978
4689 - // Get any attributes posted for variable products (where one of the dimensions is "any" for instance)
4690 - $variations = array();
4691 - 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) {
4692 4984 if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
4693 4985 continue;
4694 4986 }
4695 4987 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
@@ -4694,15 +4986,94 @@
4694 4986 }
4695 4987 $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
4696 4988 }
4697 4989
4698 - $product = null;
4699 - $variant = null;
4700 - $parent = null;
4701 - $parentid = null;
4702 - $quantity = 1;
4703 - 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 + }
4704 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 +
4705 5076 // Find the product, or variation, and get everything in order so we can check existence, availability etc. IOK 2018-10-02
4706 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
4707 5078 // This broke single-product purchases for variable products; fixed IOK 2019-05-21 thanks to Gaute Terland Nilsen @ Easyweb for the report
4708 5079 try {
@@ -4715,17 +5086,14 @@
4715 5086 $product = wc_get_product($skuid);
4716 5087 }
4717 5088 } catch (Exception $e) {
4718 5089 $result = array('ok'=>0, 'msg'=>__('Error finding product - cannot create order','woo-vipps'), 'url'=>false);
4719 - wp_send_json($result);
4720 - exit();
5090 + return $result;
4721 5091 }
4722 5092
4723 -
4724 5093 if (!$product) {
4725 5094 $result = array('ok'=>0, 'msg'=>__('Unknown product, cannot create order','woo-vipps'), 'url'=>false);
4726 - wp_send_json($result);
4727 - exit();
5095 + return $result;
4728 5096 }
4729 5097
4730 5098 $parentid = $product ? $product->get_parent_id() : null; // If the product is a variation, then the parent product is the parentid.
4731 5099 $parent = $parentid ? wc_get_product($parentid) : null;
@@ -4732,34 +5100,30 @@
4732 5100
4733 5101 // This can't really happen, but if it did..
4734 5102 if ($prodid && $parentid && ($prodid != $parentid)) {
4735 5103 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available','woo-vipps'), 'url'=>false);
4736 - wp_send_json($result);
4737 - exit();
5104 + return $result;
4738 5105 }
4739 5106 if (!$gw->product_supports_express_checkout($product)) {
4740 5107 $result = array('ok'=>0, 'msg'=>sprintf(__('%1$s is not available for this order','woo-vipps'), Vipps::ExpressCheckoutName()), 'url'=>false);
4741 - wp_send_json($result);
4742 - exit();
5108 + return $result;
4743 5109 }
4744 5110
4745 5111 // Somebody addded the wrong SKU
4746 5112 if ($product->get_type() == 'variable'){
4747 5113 $result = array('ok'=>0, 'msg'=>__('Selected product variant is not available for purchase','woo-vipps'), 'url'=>false);
4748 - wp_send_json($result);
4749 - exit();
5114 + return $result;
4750 5115 }
4751 5116 // Final check of availability
4752 5117 if (!$product->is_purchasable() || !$product->is_in_stock()) {
4753 5118 $result = array('ok'=>0, 'msg'=>__('Your product is temporarily no longer available for purchase','woo-vipps'), 'url'=>false);
4754 - wp_send_json($result);
4755 - exit();
5119 + return $result;
4756 5120 }
4757 5121
4758 5122 // Now it should be safe to continue to the checkout process. IOK 2018-10-02
4759 -
4760 5123 // Create a new temporary cart for this order. We need to get (and save) the real session cart,
4761 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
4762 5126 $current_cart = clone WC()->cart;
4763 5127 WC()->cart->empty_cart();
4764 5128
4765 5129 if ($parent && $parent->get_type() == 'variable') {
@@ -4766,50 +5130,22 @@
4766 5130 WC()->cart->add_to_cart($parent->get_id(),$quantity,$product->get_id(), $variations);
4767 5131 } else {
4768 5132 WC()->cart->add_to_cart($product->get_id(),$quantity);
4769 5133 }
5134 + WC()->session->save_data();
4770 5135
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, 'msg'=>__('Could not create order','woo-vipps') . ': ' . $e->getMessage(), 'url'=>false);
4776 - wp_send_json($result);
4777 - exit();
4778 - }
5136 + $result = $this->create_and_process_express_order();
4779 5137
4780 - if (!$orderid) {
4781 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4782 - wp_send_json($result);
4783 - 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);
4784 5145 }
4785 5146
4786 - try {
4787 - $this->maybe_add_static_shipping($gw,$orderid);
4788 - } catch (Exception $e) {
4789 - $this->log(__("Error calculating static shipping", 'woo-vipps'), 'error');
4790 - $this->log($e->getMessage(),'error');
4791 - $result = array('ok'=>0, 'msg'=>__('Could not create order','woo-vipps'), 'url'=>false);
4792 - wp_send_json($result);
4793 - exit();
4794 - }
4795 -
4796 -
4797 - // Single product purchase, so save any contents of the real cart
4798 - $order = wc_get_order($orderid);
4799 - $order->update_meta_data('_vipps_single_product_express',true);
4800 - $order->save();
4801 - $this->save_cart($order,$current_cart);
4802 -
4803 - $ok = $gw->process_payment($orderid);
4804 - if ($ok && $ok['result'] == 'success') {
4805 - $result = array('ok'=>1, 'msg'=>'', 'url'=>$ok['redirect']);
4806 - wp_send_json($result);
4807 - exit();
4808 - }
4809 - $result = array('ok'=>0, 'msg'=> sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name()), 'url'=>'');
4810 - wp_send_json($result);
4811 - exit();
5147 + return $result;
4812 5148 }
4813 5149
4814 5150 // This calculates and adds static shipping info to a partial order for express checkout if merchant has enabled this. IOK 2020-03-19
4815 5151 // Made visible for consistency with add_static_shipping. IOK 2021-10-22
@@ -4852,9 +5188,9 @@
4852 5188 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4853 5189 }
4854 5190 }
4855 5191
4856 - // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
5192 + // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4857 5193 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4858 5194 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4859 5195 // If we need to add more shipping methods *before* the shipping callback starts, it must be done before we load the session. IOK 2025-05-06
4860 5196 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
@@ -4875,9 +5211,9 @@
4875 5211 $transaction = sanitize_text_field(@$_POST['transaction']);
4876 5212
4877 5213 $sessionorders= WC()->session->get('_vipps_session_orders');
4878 5214 if (!isset($sessionorders[$orderid])) {
4879 - 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')));
4880 5216 }
4881 5217
4882 5218 $order = wc_get_order($orderid);
4883 5219 if (!$order) {
@@ -4925,46 +5261,37 @@
4925 5261 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4926 5262 return false;
4927 5263 }
4928 5264
4929 - // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4930 - // These are for the "fallback URL" mostly. IOK 2018-05-18
4931 - private function make_vipps_url($what) {
4932 - if ( !get_option('permalink_structure')) {
4933 - return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4934 - }
4935 - return trailingslashit(home_url($what, 'https'));
5265 + // The various return URLs for special pages of the Vipps stuff. Previously used a fake page and had to check permalink_structure. LP 2026-08-26
5266 + private function make_special_page_url($action) {
5267 + return add_query_arg('action', $action, $this->get_special_page_url());
4936 5268 }
5269 +
4937 5270 public function payment_return_url() {
4938 - return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
5271 + return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
4939 5272 }
4940 5273 public function express_checkout_url() {
4941 - return $this->make_vipps_url('vipps-express-checkout');
5274 + return $this->make_special_page_url('do_express_checkout');
4942 5275 }
4943 5276 public function buy_product_url() {
4944 - return $this->make_vipps_url('vipps-buy-product');
5277 + return $this->make_special_page_url('buy_product');
4945 5278 }
4946 5279
4947 - // Return the method in the Vipps
4948 - public function is_special_page() {
4949 - $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4950 - $method = null;
4951 - if ( get_option('permalink_structure')) {
4952 - foreach($specials as $special=>$specialmethod) {
4953 - // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4954 - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4955 - if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4956 - $method = $specialmethod; break;
4957 - }
4958 - }
4959 - } else {
4960 - if (isset($_GET['VippsSpecialPage'])) {
4961 - $method = @$specials[$_GET['VippsSpecialPage']];
4962 - }
4963 - }
4964 - return $method;
5280 + public static function is_special_page() {
5281 + $id = static::get_special_page_id();
5282 + return $id && is_page($id);
4965 5283 }
4966 5284
5285 + public static function get_special_page_id() {
5286 + $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5287 + return $id > 0 ? $id : null;
5288 + }
5289 +
5290 + public static function get_special_page_url() {
5291 + return get_permalink(static::get_special_page_id());
5292 + }
5293 +
4967 5294 // Just create a spinner and a overlay.
4968 5295 public function spinner () {
4969 5296 $flavour = sanitize_title($this->get_payment_method_name());
4970 5297 ob_start();
@@ -5011,16 +5338,8 @@
5011 5338 }
5012 5339 return null;
5013 5340 }
5014 5341
5015 - // DEPRECATED: Legacy function as of new web component express buttons. see get_buy_now_button and get_html_button. LP 2026-06-26
5016 - public function get_buy_now_button_manual($product_id, $variation_id=null, $sku=null, $disabled=false, $classes='',
5017 - $_logo_variant=null, $_logo_lang=null, // deprecated params
5018 - $context='global', $button_args_override = [],
5019 - ) {
5020 - return $this->get_buy_now_button($product_id, $variation_id, $sku, $disabled, $classes, $context, $button_args_override);
5021 - }
5022 -
5023 5342 // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5024 5343 // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5025 5344 // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5026 5345 public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
@@ -5064,8 +5383,11 @@
5064 5383 if ($classes) $classes = " $classes";
5065 5384 if ($short) $classes = "short $classes";
5066 5385
5067 5386 $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5387 +
5388 +
5389 +
5068 5390 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
5069 5391 }
5070 5392
5071 5393 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
@@ -5148,51 +5470,90 @@
5148 5470 echo "<div class='vipps_buy_now_wrapper loop'>$button</div>";
5149 5471 }
5150 5472
5151 5473
5152 -
5153 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5474 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5154 5475 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5476 + // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5155 5477 public function woocommerce_create_pages ($data) {
5478 + // Vipps Checkout page
5156 5479 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5157 - if (!$vipps_checkout_activated) return $data;
5480 + if ($vipps_checkout_activated) {
5481 + $data['vipps_checkout'] = array(
5482 + 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5483 + 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5484 + 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5485 + );
5486 + }
5158 5487
5159 - $data['vipps_checkout'] = array(
5160 - 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5161 - 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5162 - 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5163 - );
5164 -
5488 + // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5489 + $data['vipps_special_page'] = [
5490 + 'name' => 'vipps-payment', // slug
5491 + /* translators: company name */
5492 + 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5493 + 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5494 + ];
5165 5495 return $data;
5166 5496 }
5167 5497
5168 - // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5498 + // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5499 + // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5169 5500 public function maybe_create_vipps_pages () {
5501 + $make_pages = false;
5502 +
5503 + // Vipps Checkout page. LP 2026-08-18
5170 5504 $checkoutid = wc_get_page_id('vipps_checkout');
5171 - $makeit = !$checkoutid || ! get_post_status($checkoutid);
5172 - if ($makeit) {
5505 + if (!$checkoutid || ! get_post_status($checkoutid)) {
5173 5506 delete_option('woocommerce_vipps_checkout_page_id');
5507 + $make_pages = true;
5174 5508 }
5175 5509
5176 - if ($makeit) {
5177 - WC_Install::create_pages();
5510 + // vipps special page, previously a fake page. LP 2026-08-18
5511 + $builtin_special_page_id = static::get_special_page_id();
5512 + if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5513 + delete_option('woocommerce_vipps_special_page_page_id');
5514 + $make_pages = true;
5178 5515 }
5516 +
5517 + if ($make_pages) {
5518 + WC_Install::create_pages();
5519 + }
5179 5520 }
5180 5521
5522 + public function vipps_special_page_shortcode($atts, $content) {
5523 + // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5524 + if (is_admin()) return;
5525 + if (wp_doing_ajax()) return;
5526 + if (defined('REST_REQUEST') && REST_REQUEST) return;
5527 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5181 5528
5529 + $action = $_GET['action'] ?? '';
5530 + $html = "";
5531 + switch ($action) {
5532 + case 'wait_for_payment':
5533 + $html = $this->vipps_wait_for_payment();
5534 + break;
5535 + case 'do_express_checkout':
5536 + $html = $this->vipps_express_checkout();
5537 + break;
5538 + case 'buy_product':
5539 + $html = $this->vipps_buy_product();
5540 + break;
5541 + default:
5542 + $html = '';
5543 + }
5544 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5545 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5546 +
5547 + // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5548 + return $html;
5549 + }
5550 +
5551 +
5182 5552 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5183 5553 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5184 5554 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5185 5555 public function vipps_buy_product() {
5186 - status_header(200,'OK');
5187 - Vipps::nocache();
5188 -
5189 - add_filter('body_class', function ($classes) {
5190 - $classes[] = 'vipps-express-checkout';
5191 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5192 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5193 - });
5194 -
5195 5556 do_action('woo_vipps_express_checkout_page');
5196 5557
5197 5558 $session = WC()->session;
5198 5559 $posted = $session->get('__vipps_buy_product');
@@ -5221,39 +5582,38 @@
5221 5582
5222 5583 if (!$productinfo) {
5223 5584 $title = __("Product is no longer available",'woo-vipps');
5224 5585 $content = __("The link you have followed is for a product that is no longer available at this location. Please return to the store and try again",'woo-vipps');
5225 - return $this->fakepage($title,$content);
5586 + return $this->special_page_html($title,$content);
5226 5587 }
5227 5588
5228 5589 // Pass the productinfo to the express checkout form
5229 5590 $args = array();
5230 - $args['quantity'] = 1;
5231 - if (array_key_exists('product_id',$productinfo)) $args['product_id'] = intval($productinfo['product_id']);
5232 - if (array_key_exists('variation_id',$productinfo)) $args['variation_id'] = intval($productinfo['variation_id']);
5233 - if (array_key_exists('product_sku',$productinfo)) $args['sku'] = sanitize_text_field($productinfo['product_sku']);
5234 - 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)));
5235 5595
5236 - // 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.
5237 - foreach ($productinfo as $key => $value) {
5238 - if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
5239 - continue;
5240 - }
5241 - $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5242 - }
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);
5243 5599
5244 - $this->print_express_checkout_page(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;
5245 5609 }
5246 5610
5247 - // This is a landing page for the express checkout of then normal cart - it is done like this because this could take time on slower hosts.
5248 - public function vipps_express_checkout() {
5249 - status_header(200,'OK');
5250 - Vipps::nocache();
5611 + public function vipps_express_checkout_consistency_check() {
5251 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.
5252 5613 // IOK 2018-05-28
5253 5614 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5254 5615
5255 -
5256 5616 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5257 5617 if (!$backurl) $backurl = home_url();
5258 5618
5259 5619 if (!$ok) {
@@ -5267,219 +5627,39 @@
5267 5627 wp_redirect($backurl);
5268 5628 exit();
5269 5629 }
5270 5630
5271 - add_filter('body_class', function ($classes) {
5272 - $classes[] = 'vipps-express-checkout';
5273 - $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5274 - return apply_filters('woo_vipps_express_checkout_body_class', $classes);
5275 - });
5276 -
5277 - do_action('woo_vipps_express_checkout_page');
5278 -
5279 - $this->print_express_checkout_page(true, 'do_express_checkout');
5631 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5280 5632 }
5281 5633
5282 - // This method tries to ensure that a customer does not 'lose' the return page and
5283 - // starts ordering the same products twice. IOK 2020-01-22
5284 - protected function validate_express_checkout_orderspec ($orderspec) {
5285 - if (empty($orderspec)) return true; // It's not a duplicate, it's nothing.
5286 -
5287 - // First build for the current order an array of hash-tables keyed by prodid, varid and quantity.
5288 - $orderset = array();
5289 - foreach($orderspec as $entry) $orderset[] = join(':', $entry);
5290 -
5291 - // Then get open orders
5292 - $sessionorders = array();
5293 - $sessionorderdata = WC()->session->get('_vipps_session_orders');
5294 - if ($sessionorderdata) {
5295 - foreach(array_keys($sessionorderdata) as $oid) {
5296 - $orderobject = wc_get_order($oid);
5297 - // Check to see that this hasn't been deleted yet IOK 2020-01-07
5298 - if ($orderobject instanceof WC_Order) {
5299 - $sessionorders[] = $orderobject;
5300 - }
5301 - }
5634 + // This is a landing page for the express checkout of the normal cart - it is done like this because this could take time on slower hosts.
5635 + // IOK 2026-09-09 - nowadays this is only used for compatibility mode. It will automatically start express checkout of the current cart when reached.
5636 + public function vipps_express_checkout() {
5637 + // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5638 + if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5639 + $content = __('Link expired, please try again', 'woo-vipps');
5640 + return $content;
5302 5641 }
5303 - // Nothing more to do here
5304 - if (empty($sessionorders)) return true;
5642 +
5643 + do_action('woo_vipps_express_checkout_page');
5305 5644
5306 - // And create a similar hash table for each of the open orders
5307 - $openorderdata = array();
5308 - foreach ($sessionorders as $open_order) {
5309 - $status = $open_order->get_status();
5310 - if ($status == 'cancelled' || $status == 'pending') continue;
5311 - $when = strtotime($open_order->get_date_modified());
5312 - $cutoff = $when + apply_filters('woo_vipps_recent_order_cutoff', (5*60));
5313 - if (time() > $cutoff) {
5314 - continue;
5315 - }
5316 - $orderdata = array();
5317 - foreach($open_order->get_items() as $item) {
5318 - $productspec = $item->get_product_id() . ':' . $item->get_variation_id() . ':' . $item->get_quantity();
5319 - $orderdata[] = $productspec;
5320 - }
5321 - $openorderdata[]=$orderdata;
5322 - }
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>";
5323 5656
5324 - // 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.
5325 - foreach($openorderdata as $prevorder) {
5326 - $a = array_diff($prevorder, $orderset);
5327 - $b = array_diff($orderset, $prevorder);
5328 - if (empty($a) && empty($b)) {
5329 - $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');
5330 - return false;
5331 - }
5332 - }
5333 - // Else, order is good.
5334 - return true;
5657 + return $content;
5335 5658 }
5336 5659
5337 - // Returns a triple of productid, variantid and quantity from an array of arguments which can pass either these or a SKU value.
5338 - // Return value is like in a cart.
5339 - // Used to create an order in express checkout, and to see that this order isn't a repeat. IOK 2020-01-22
5340 - protected function get_orderspec_from_arguments ($productinfo) {
5341 - if (!$productinfo) return array();
5342 - $variantid = 0;
5343 - $productid = 0;
5344 - $quantity = intval(@$productinfo['quantity']);
5345 - if (!$quantity) $quantity = 1;
5346 - if (isset($productinfo['sku']) && $productinfo['sku']) {
5347 - $sku = $productinfo['sku'];
5348 - $skuid = wc_get_product_id_by_sku($sku);
5349 - $product = wc_get_product($skuid);
5350 - $parentid = $product ? $product->get_parent_id() : null;
5351 - if ($product) {
5352 - if ($parentid) {
5353 - $variantid = $skuid; $productid = $parentid;
5354 - } else {
5355 - $productid = $skuid;
5356 - }
5357 - }
5358 - } else if (isset($productinfo['product_id']) && $productinfo['product_id']) {
5359 - $productid = intval($productinfo['product_id']);
5360 - $variantid = intval(@$productinfo['variation_id']);
5361 - }
5362 - if ($productid) return array(array('product_id'=>$productid, 'variation_id'=>$variantid, 'quantity'=>$quantity));
5363 - return array();
5364 - }
5365 - // If no productinfo, this will produce an orderspec from the current cart IOK 2020-01-24
5366 - protected function get_orderspec_from_cart () {
5367 - $cartitems = WC()->cart->get_cart();
5368 - $orderspec = array();
5369 - foreach($cartitems as $item => $values) {
5370 - $orderspec[] = array('product_id'=>$values['product_id'], 'variation_id'=>$values['variation_id'], 'quantity'=>$values['quantity']);
5371 - }
5372 - return $orderspec;
5373 - }
5374 -
5375 - // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5376 - protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5377 - $gw = $this->gateway();
5378 -
5379 - $expressCheckoutMessages = array();
5380 - $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
5381 - $expressCheckoutMessages['temporaryError'] = sprintf(__('%1$s is temporarily unavailable.','woo-vipps'), $this->get_payment_method_name());
5382 - $expressCheckoutMessages['successMessage'] = sprintf(__('To the %1$s app!','woo-vipps'), $this->get_payment_method_name());
5383 -
5384 - 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');
5385 - wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5386 - wp_enqueue_script('vipps-express-checkout');
5387 - // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5388 - // to actually perform the express checkout.
5389 - $buttonhtml = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button());
5390 -
5391 -
5392 -
5393 - $orderspec = $this->get_orderspec_from_arguments($productinfo);
5394 - if (empty($orderspec)) {
5395 - $orderspec = $this->get_orderspec_from_cart();
5396 - }
5397 - $orderisOK = $this->validate_express_checkout_orderspec($orderspec);
5398 - $orderisOK = apply_filters('woo_vipps_validate_express_checkout_orderspec', $orderisOK, $orderspec);
5399 -
5400 - $askForTerms = function_exists('wc_terms_and_conditions_checkbox_enabled') ? wc_terms_and_conditions_checkbox_enabled() : true;
5401 - $askForTerms = $askForTerms && ($gw->get_option('expresscheckout_termscheckbox') == 'yes');
5402 - $askForTerms = apply_filters('woo_vipps_express_checkout_terms_and_conditions_checkbox_enabled', $askForTerms);
5403 -
5404 - $askForConfirmationHTML = '';
5405 - if (!$orderisOK) {
5406 - $header = __("Are you sure?",'woo-vipps');
5407 - $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');
5408 - $askForConfirmationHTML = apply_filters('woo_vipps_ask_user_to_confirm_repurchase', "<h2 class='confirmVippsExpressCheckoutHeader'>$header</h2><p>$body</p>");
5409 - }
5410 - // Should we go directly to checkout, or do we need to stop and ask the user something (for instance?) IOK 2010-01-20
5411 - $execute = $execute && $orderisOK && !$askForTerms;
5412 - $execute = apply_filters('woo_vipps_checkout_directly_to_vipps', $execute, $productinfo);
5413 -
5414 - $content = $this->spinner();
5415 -
5416 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5417 - // The form data below is sent on order creation; the sec is also used to poll session status
5418 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5419 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5420 - $content .= "<input type='hidden' name='action' value='" . esc_attr($action) ."'>";
5421 - if ($this->gateway()->get_option('vippsorderattribution') == 'yes') {
5422 - // This is for the new order attribution feature of woo. IOK 2024-01-09
5423 - $content .= '<input type="hidden" id="vippsorderattribution" value="1" />';
5424 - ob_start();
5425 - do_action( 'woocommerce_after_order_notes');
5426 - $content .= ob_get_clean();
5427 - }
5428 - $content .= wp_nonce_field('do_express','sec',1,false);
5429 -
5430 - $termsHTML = '';
5431 - if ($askForTerms) {
5432 - // Include shop terms
5433 - ob_start();
5434 - wc_get_template('checkout/terms.php');
5435 - $termsHTML = ob_get_clean();
5436 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5437 - }
5438 - $termsHTML = apply_filters('woo_vipps_express_checkout_terms_and_conditions_html',$termsHTML);
5439 -
5440 - if ($productinfo) {
5441 - foreach($productinfo as $key=>$value) {
5442 - $k = esc_attr($key);
5443 - $v = esc_attr($value);
5444 - $content .= "<input type='hidden' name='$k' value='$v' />";
5445 - }
5446 - }
5447 - ob_start();
5448 - $content .= do_action('woo_vipps_express_checkout_orderspec_form', $productinfo);
5449 - $content .= ob_get_clean();
5450 - $content .= "</form>";
5451 -
5452 - $extraHTML = apply_filters('woo_vipps_express_checkout_final_html', '', $termsHTML,$askForConfirmationHTML);
5453 - $pressTheButtonHTML = "";
5454 - if (empty($termsHTML) && empty($askForConfirmationHTML) && empty($extraHTML)) {
5455 - $pressTheButtonHTML = "<p id=waiting>" . sprintf(__('Ready for %1$s - press the button', 'woo-vipps'), Vipps::ExpressCheckoutName()) . "</p>";
5456 - }
5457 -
5458 - if ($execute) {
5459 - $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5460 - $content .= "<div id='vipps-status-message'></div>";
5461 - $this->fakepage(__('Order in progress','woo-vipps'), $content);
5462 - return;
5463 - } else {
5464 - $content .= $askForConfirmationHTML;
5465 - $content .= $extraHTML;
5466 - $content .= $termsHTML;
5467 - $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5468 - $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5469 - $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>";
5470 - $content .= "<div id='vipps-status-message'></div>";
5471 - $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5472 - return;
5473 - }
5474 - }
5475 -
5476 -
5477 -
5478 - public function vipps_wait_for_payment() {
5479 - status_header(200,'OK');
5480 - Vipps::nocache();
5481 -
5660 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5661 + private function handle_payment_poll_and_redirect () {
5482 5662 $orderid = WC()->session->get('_vipps_pending_order');
5483 5663
5484 5664 $order = null;
5485 5665 $gw = $this->gateway();
@@ -5493,9 +5673,9 @@
5493 5673 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5494 5674 // simulating the session with that.
5495 5675 // IOK 2019-11-19, changed to using GET 2023-01-23
5496 5676 if ($no_session && $limited_session) {
5497 - $orderid = intval(@$_GET['id']);
5677 + $orderid = intval($_GET['id'] ?? false);
5498 5678 }
5499 5679 if ($orderid) {
5500 5680 clean_post_cache($orderid);
5501 5681 $order = wc_get_order($orderid);
@@ -5510,20 +5690,22 @@
5510 5690 $session = WC()->session;
5511 5691 if (!$session->has_session()) {
5512 5692 $session->set_customer_session_cookie(true);
5513 5693 }
5694 +
5695 + $sessionorders= WC()->session->get('_vipps_session_orders');
5696 + $sessionorders[$orderid] = 1;
5697 + WC()->session->set('_vipps_session_orders',$sessionorders);
5514 5698 $session->set('_vipps_pending_order', $orderid);
5699 + WC()->session->save_data();
5515 5700 }
5516 5701 }
5517 5702
5518 -
5519 - do_action('woo_vipps_wait_for_payment_page',$order);
5520 -
5521 5703 $deleted_order=0;
5522 5704 if ($orderid && !$order) {
5523 5705 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5524 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
5525 - $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');
5526 5708 $deleted_order=1;
5527 5709 }
5528 5710
5529 5711 if (!$order && !$deleted_order) wp_die(__('Unknown order', 'woo-vipps'));
@@ -5533,12 +5715,13 @@
5533 5715
5534 5716 // This is for debugging only - set to false to ensure we wait for the callback. IOK 2023-08-04
5535 5717 $do_poll = true;
5536 5718
5537 - // 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
5538 5720 if ($do_poll && $status == 'pending') {
5539 - // Just in case the callback hasn't come yet, do a quick check of the order status at Vipps.
5540 - $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');
5541 5724 if ($status != $newstatus) {
5542 5725 $status = $newstatus;
5543 5726 clean_post_cache($orderid);
5544 5727 $order = wc_get_order($orderid); // Reload order object
@@ -5543,11 +5726,13 @@
5543 5726 clean_post_cache($orderid);
5544 5727 $order = wc_get_order($orderid); // Reload order object
5545 5728 }
5546 5729 } else {
5547 - // No need to do anyting here. IOK 2020-01-26
5730 + // No need to do anyting here. IOK 2020-01-26
5548 5731 }
5549 5732
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
5550 5735 $payment = 'notchecked';
5551 5736 if ($do_poll) {
5552 5737 $payment = $deleted_order ? 'cancelled' : $gw->check_payment_status($order);
5553 5738 }
@@ -5561,9 +5746,8 @@
5561 5746 exit();
5562 5747 }
5563 5748
5564 5749 // We are done, but in failure. Don't poll.
5565 - $content = "";
5566 5750 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5567 5751
5568 5752 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5569 5753 // For failed it shows a "Retry payment" button that takes the customer to /pay-for-order where it will be retried. LP 2026-03-17
@@ -5571,8 +5755,9 @@
5571 5755 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5572 5756 wp_redirect($failure_redirect);
5573 5757 exit();
5574 5758 }
5759 +
5575 5760 if ($status == 'cancelled' || $payment == 'cancelled') {
5576 5761 $this->maybe_restore_cart($orderid,'failed');
5577 5762 if ($failure_redirect){
5578 5763 wp_redirect($failure_redirect);
@@ -5577,27 +5762,46 @@
5577 5762 if ($failure_redirect){
5578 5763 wp_redirect($failure_redirect);
5579 5764 exit();
5580 5765 }
5766 + } else {
5767 + // If not, enqueue the status checker IOK 2026-09-21
5768 + wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5769 + }
5770 +
5771 + $this->log(sprintf(__("Order status of %1\$d not ready in order return: payment status %2\$s", 'woo-vipps'), $orderid, $payment), 'info');
5772 +
5773 + // Communicate this to the shortcode IOK 2026-09-21
5774 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5775 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5776 + });
5777 +
5778 + }
5779 +
5780 + public function vipps_wait_for_payment() {
5781 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5782 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5783 +
5784 + $orderid = $data['orderid'] ?? 0;
5785 + $status = $data['status'] ?? "";
5786 + $payment = $data['payment'] ?? "";
5787 +
5788 + $order = wc_get_order($orderid);
5789 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5790 +
5791 + do_action('woo_vipps_wait_for_payment_page',$order);
5792 + $gw = $this->gateway();
5793 +
5794 + $content = "";
5795 + if ($status == 'cancelled' || $payment == 'cancelled') {
5581 5796 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5582 5797 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5583 5798 $content .= "</div>";
5584 - $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5585 -
5586 - return;
5799 + return $this->special_page_html('', $content);
5587 5800 }
5588 5801
5589 5802 // Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27
5590 -
5591 5803 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5592 - wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5593 -
5594 - // Check that order exists and belongs to our session. Can use WC()->session->get() I guess - set the orderid or a hash value in the session
5595 - // and check that the order matches (and is 'pending') (and exists)
5596 - $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5597 - $vippsstatus = $order->get_meta('_vipps_status');
5598 - $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5599 -
5600 5804 $signal = $this->callbackSignal($order);
5601 5805 $content = "";
5602 5806 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5603 5807
@@ -5605,18 +5809,18 @@
5605 5809 $signalurl = $this->callbackSignalURL($signal);
5606 5810
5607 5811 $content .= "</p></div>";
5608 5812
5609 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5610 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5611 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5813 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5814 +
5815 + // Carry the order status to the checking script IOK 2026-09-21
5816 + $content .= "<form id='vippsdata'>";
5612 5817 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5613 5818 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5614 5819 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5615 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5820 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5616 5821 $content .= "</form>";
5617 5822
5618 -
5619 5823 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
5620 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>";
5621 5825 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5622 5826 $content .= "</div>";
@@ -5630,93 +5834,21 @@
5630 5834 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5631 5835 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5632 5836 $content .= "</div>";
5633 5837
5838 + return $this->special_page_html('', $content);
5839 + }
5634 5840
5635 - $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5841 + // Returns formatted html for the vipps special page. LP 2026-08-27
5842 + public function special_page_html($header, $content) {
5843 + $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5844 + $html = <<<EOF
5845 + $header_html
5846 + <div class="vipps-special-page-content">$content</div>
5847 + EOF;
5848 + return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5636 5849 }
5637 5850
5638 -
5639 -
5640 - public function fakepage($title,$content) {
5641 - global $wp, $wp_query;
5642 - // We don't want this here.
5643 - remove_filter ('the_content', 'wpautop');
5644 -
5645 - $specialid = $this->gateway()->get_option('vippsspecialpageid');
5646 - $wp_post = null;
5647 - if ($specialid) {
5648 - $wp_post = get_post($specialid);
5649 - if ($wp_post) {
5650 - $wp_post->post_title = $title;
5651 - $wp_post->post_content = $content;
5652 - // Normalize a bit
5653 - $wp_post->filter = 'raw'; // important
5654 - $wp_post->post_status = 'publish';
5655 - $wp_post->comment_status= 'closed';
5656 - $wp_post->ping_status= 'closed';
5657 - } else {
5658 - $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5659 - }
5660 - }
5661 - if (!$wp_post || is_wp_error($wp_post)) {
5662 - $post = new stdClass();
5663 - $post->ID = -99;
5664 - $post->post_author = 1;
5665 - $post->post_date = current_time( 'mysql' );
5666 - $post->post_date_gmt = current_time( 'mysql', 1 );
5667 - $post->post_title = $title;
5668 - $post->post_content = $content;
5669 - $post->post_status = 'publish';
5670 - $post->comment_status = 'closed';
5671 - $post->ping_status = 'closed';
5672 - $post->post_name = 'vippsconfirm-fake-page-name';
5673 - $post->post_type = 'page';
5674 - $post->filter = 'raw'; // important
5675 - $wp_post = new WP_Post($post);
5676 - wp_cache_add( -99, $wp_post, 'posts' );
5677 - }
5678 -
5679 - // Update the main query
5680 - $wp_query->post = $wp_post;
5681 - $wp_query->posts = array( $wp_post );
5682 - $wp_query->queried_object = $wp_post;
5683 - $wp_query->queried_object_id = $wp_post->ID;
5684 - $wp_query->found_posts = 1;
5685 - $wp_query->post_count = 1;
5686 - $wp_query->max_num_pages = 1;
5687 - $wp_query->is_page = true;
5688 - $wp_query->is_singular = true;
5689 - $wp_query->is_single = false;
5690 - $wp_query->is_attachment = false;
5691 - $wp_query->is_archive = false;
5692 - $wp_query->is_category = false;
5693 - $wp_query->is_tag = false;
5694 - $wp_query->is_tax = false;
5695 - $wp_query->is_author = false;
5696 - $wp_query->is_date = false;
5697 - $wp_query->is_year = false;
5698 - $wp_query->is_month = false;
5699 - $wp_query->is_day = false;
5700 - $wp_query->is_time = false;
5701 - $wp_query->is_search = false;
5702 - $wp_query->is_feed = false;
5703 - $wp_query->is_comment_feed = false;
5704 - $wp_query->is_trackback = false;
5705 - $wp_query->is_home = false;
5706 - $wp_query->is_embed = false;
5707 - $wp_query->is_404 = false;
5708 - $wp_query->is_paged = false;
5709 - $wp_query->is_admin = false;
5710 - $wp_query->is_preview = false;
5711 - $wp_query->is_robots = false;
5712 - $wp_query->is_posts_page = false;
5713 - $wp_query->is_post_type_archive = false;
5714 - // Update globals
5715 - $GLOBALS['wp_query'] = $wp_query;
5716 - $wp->register_globals();
5717 - return $wp_post;
5718 - }
5719 5851
5720 5852 // Support the interactivity API with data about our cart IOK 2026-02-23
5721 5853 public function woo_vipps_store_api_cart_data() {
5722 5854 // Reverting the condition with the directive data-wp-bind--hidden does not work, so we need the flipped bool here (hide instead of show). LP 2026-02-10