PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.1
Pay with Vipps and MobilePay for WooCommerce v6.2.1
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 5.4.3 5.4.2 5.4.1 5.4.0 5.3.4 All 184 releases
← All changes | payment/Vipps.class.php +961 -658 5.4.06.2.1 View file →
@@ -36,8 +36,12 @@
36 36 }
37 37 require_once(dirname(__FILE__) . "/VippsAPIException.class.php");
38 38
39 39 class Vipps {
40 + /* Rest api consts. LP 2026-03-30 */
41 + private const REST_NAMESPACE_BASE = 'woo-vipps';
42 + private const REST_CURRENT_VERSION = 'v1'; // don't use this directly, use methods get_rest_namespace() and get_rest_url(). LP 2026-04-22
43 +
40 44 private static $instance = null;
41 45
42 46 /* Used to interact with other payment gateways if neccessary (for 'external payment gateways') IOK 2024-05-27 */
43 47 public static $installed_gateways = [];
@@ -55,8 +59,11 @@
55 59 private $lockKey = null;
56 60
57 61 public $vippsJSConfig = array();
58 62
63 + public $button_options_version = '2.0';
64 + public $button_options_express_version = '2.0';
65 +
59 66 // IOK 2023-11-29 Vipps merging with MobilePay causes some challenges which we solve by abstraction
60 67 public static function CompanyName() {
61 68 return __("Vipps MobilePay", 'woo-vipps');
62 69 }
@@ -63,9 +70,9 @@
63 70 public static function CheckoutName($order=null) {
64 71 return "Vipps MobilePay Checkout"; // Do not translate
65 72 }
66 73 public static function ExpressCheckoutName($order=null) {
67 - return __("Vipps Express Checkout", 'woo-vipps');
74 + return __("Vipps MobilePay Express Checkout", 'woo-vipps');
68 75 }
69 76 public static function LoginName() {
70 77 return __("Login with Vipps", 'woo-vipps');
71 78 }
@@ -74,8 +81,15 @@
74 81 if (!static::$instance) static::$instance = new Vipps();
75 82 return static::$instance;
76 83 }
77 84
85 + // recognize our own gateways or gateway ids IOK 2026-05-26
86 + // param is either order or order's payment method id string. LP 2026-05-28
87 + public static function is_vipps_order($order_or_string) {
88 + $id = is_string($order_or_string) ? $order_or_string : $order_or_string->get_payment_method();
89 + return in_array($id , ['vipps', 'vipps_card']);
90 + }
91 +
78 92 // To simplify development, we load translations from the plugins' own .mos on development branches. IOK 2023-11-28
79 93 public static function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) {
80 94 $development = apply_filters('woo_vipps_use_plugin_translations', false);
81 95 if (!$development) {
@@ -107,12 +121,17 @@
107 121 add_action('init',array($Vipps,'init'));
108 122 add_action( 'woocommerce_loaded', array($Vipps,'woocommerce_loaded'));
109 123 add_filter( 'woocommerce_available_payment_gateways', array($Vipps, 'payment_gateway_filter'));
110 124 add_action( 'woocommerce_blocks_loaded', [$Vipps, 'woocommerce_blocks_loaded']);
111 - // Express Checkout and Vipps Checkout supports the new pickup_location shipping method, but the admin interface for this may
125 + // Express Checkout and Checkout supports the new pickup_location shipping method, but the admin interface for this may
112 126 // 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
113 127 // stored in the database since we support this for both Vipps MobilePay checkokut and Express. IOK 2026-02-25
114 128 add_action('woocommerce_load_shipping_methods', array($Vipps, 'maybe_load_pickup_locations'), 90);
129 +
130 + // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
131 + // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
132 + // is important.
133 + add_filter('woocommerce_create_pages', array($Vipps, 'woocommerce_create_pages'), 50, 1);
115 134 }
116 135
117 136 // Register woocommerce store api endpoint to use in buy-now minicart block. LP 2026-02-10
118 137 public function woocommerce_blocks_loaded() {
@@ -135,9 +154,11 @@
135 154 public function payment_gateway_filter ($gateways) {
136 155 if (is_checkout_pay_page()) {
137 156 $orderid = absint(get_query_var( 'order-pay'));
138 157 $order = $orderid ? wc_get_order($orderid) : null;
139 - if (is_a($order, 'WC_Order')) {
158 + if (is_a($order, 'WC_Order')
159 + && $order->get_meta('_vipps_init_timestamp') // allow vipps payment for new orders, like when creating an order from backend. LP 2026-05-28
160 + ) {
140 161 // Existing override that allows repayment. IOK 2024-06-04
141 162 // i.e a third party plugin that implemented payment retrying for our plugin, we used to enable repayment only if this plugin was found.
142 163 // $allow_repayment = class_exists('\Site\Plugins\WooVipps\WooVippsPayForOrder');
143 164 // However, now we implement payment retrying ourselves. LP 2026-03-18
@@ -225,9 +246,9 @@
225 246 add_action('wp_ajax_woo_vipps_order_action', array($this, 'order_handle_vipps_action'));
226 247
227 248 // Fetch wc products, but filter those only purchasable by VMP express checkout. LP 2026-01-22
228 249 add_action('rest_api_init', function() {
229 - register_rest_route('woo-vipps/v1', '/express-products', [
250 + register_rest_route(self::get_rest_namespace('v1'), '/express-products', [
230 251 'methods' => 'GET',
231 252 'callback' => [$this, 'rest_express_checkout_products'],
232 253 'permission_callback' => '__return_true',
233 254 ]);
@@ -262,39 +283,46 @@
262 283 add_filter('woo_vipps_lock_order', array($this,'flock_lock_order'));
263 284 add_action('woo_vipps_unlock_order', array($this, 'flock_unlock_order'));
264 285 }
265 286
266 - }
287 + // Set default button options, migrating any older setup IOK 2026-07-15
288 + $this->init_button_options();
267 289
290 + /*
291 + From version 6.2.x we create a real physical page to handle the "special" vipps pages,
292 + where we earlier used just a fake page with no real page id, unless especially configured.
293 + We therefore need to add code to maintain this special page.
268 294
269 - // IOK 2022-12-02 This is currently used in two places: In the code that finds orders marked "to be deleted", and
270 - // in the getOrderIdByVippsOrderId function. This is for the old-style Woo order tables, and do nothing for HPOS right now.
271 - // This should however be replaced by its own table so it can be done more efficiently.
272 - public static function add_wc_order_meta_key_support() {
273 - if (did_action('woo_vipps_add_order_meta_key_support')) return;
274 - do_action('woo_vipps_add_order_meta_key_support');
275 - add_filter('woocommerce_order_data_store_cpt_get_orders_query', function ($query, $query_vars) {
276 - if (isset($query_vars['meta_vipps_orderid']) && $query_vars['meta_vipps_orderid'] ) {
277 - if (!isset($query['meta_query'])) $query['meta_query'] = array();
278 - $query['meta_query'][] = array(
279 - 'key' => '_vipps_orderid',
280 - 'value' => $query_vars['meta_vipps_orderid']
281 - );
295 + woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
296 + and we hook unto this with woocommerce_create_pages. LP 2026-09-03
297 + */
298 +
299 + // Delete special page id option when its deleted or trashed, so that we dont have to load
300 + // in the post to check status in woocommerce_loaded when we ensure the special page exists. LP 2026-09-03
301 + $delete_special_page_id = function($post_id, $post = null) {
302 + if (static::get_special_page_id() === $post_id) {
303 + delete_option('woocommerce_vipps_special_page_page_id');
282 304 }
283 - if (isset($query_vars['meta_vipps_delendum']) && $query_vars['meta_vipps_delendum'] ) {
284 - if (!isset($query['meta_query'])) $query['meta_query'] = array();
285 - $query['meta_query'][] = array(
286 - 'key' => '_vipps_delendum',
287 - 'value' => 1
288 - );
289 - }
290 - return $query;
291 - }, 10, 2);
305 + };
306 + add_action('delete_post', $delete_special_page_id, 10, 2);
307 + add_action('wp_trash_post', $delete_special_page_id, 10, 2);
292 308
309 + $this->ensure_special_page_exists();
310 +
311 +
312 + // We want this special page to have a certain title and maybe special scripts and so on,
313 + // this gets run in template redirect for these pages.
314 + add_action('woo_vipps_before_handling_special_page', function ($action) {
315 + // Change title dynamically depending on action. LP 2026-09-02
316 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
317 + });
318 +
319 + // Add an admin interface for this page as well IOK 2026-09-11
320 + add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
321 +
293 322 }
294 323
295 324 public function admin_init () {
296 -
297 325 $gw = $this->gateway();
298 326 require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php");
299 327 $adminSettings = VippsAdminSettings::instance();
300 328 // Stuff for the Order screen
@@ -303,9 +331,9 @@
303 331 // Don't allow deletion of refunds made through Vipps IOK 2025-11-17
304 332 add_action('woocommerce_after_order_refund_item_name', function ($refund) {
305 333 $orderid = $refund->get_parent_id();
306 334 $order = wc_get_order($orderid);
307 - if (is_a($order, 'WC_Order') && $order->get_payment_method() == 'vipps') {
335 + if (is_a($order, 'WC_Order') && self::is_vipps_order($order)) {
308 336 $id = $refund->get_id();
309 337 $gw = $refund->get_refunded_payment();
310 338 if ($gw) {
311 339 $msg = sprintf(__('Refunded through %1$s', 'woo-vipps'), $this->get_payment_method_name());
@@ -324,20 +352,21 @@
324 352 $this->vippsJSConfig['vippssecnonce'] = wp_create_nonce('vippssecnonce');
325 353 wp_localize_script('vipps-gw', 'VippsConfig', $this->vippsJSConfig);
326 354 add_action('admin_enqueue_scripts', array($this,'admin_enqueue_scripts'));
327 355
328 - // Redirect the default WooCommerce settings page to our own
329 - add_action( 'woocommerce_settings_start', function () {
330 - add_filter('admin_url', function ($url, $path) {
331 - if (strpos($path, "tab=checkout&section=vipps") === false) return $url;
332 - $qs = parse_url($path, PHP_URL_QUERY);
333 - if (!$qs) return $url;
334 - $args = [];
335 - parse_str($qs, $args);
336 - $ok = (($args['page']??false) == 'wc-settings') && (($args['tab']??false) == 'checkout') && (($args['section']??false) == 'vipps');
337 - if (!$ok) return $url;
338 - return admin_url("/admin.php?page=vipps_settings_menu");
339 - }, 10, 2);
356 + // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
357 + add_action('current_screen', function ($screen) {
358 + if (!is_admin() || !$screen || $screen->id !== 'woocommerce_page_wc-settings') return;
359 + $section = ($_GET['section'] ?? "");
360 + if (($_GET['tab'] ?? "")!= 'checkout'
361 + || !in_array($section, ['vipps', 'vipps_card'])
362 + ) return;
363 +
364 + $settings_tab = '';
365 + if ('vipps_card' === $section) $settings_tab = '#Card payments';
366 +
367 + wp_safe_redirect(admin_url("admin.php?page=vipps_settings_menu$settings_tab"));
368 + exit();
340 369 });
341 370
342 371 // Custom product properties
343 372 // IOK 2024-01-17 temporary: The special product properties are currenlty only active for Vipps
@@ -410,9 +439,77 @@
410 439 }
411 440 }
412 441 }
413 442
443 +
444 + /** Ensure we have a special page for payment flows
445 + *
446 + * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
447 + * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
448 + **/
449 + public function ensure_special_page_exists() {
450 + if (static::get_special_page_id()) return;
451 + $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
414 452
453 + // 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
454 + $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
455 + if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
456 + $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
457 + // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
458 + update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
459 +
460 + // Ensure this page has the necessary shortcode. LP 2026-09-01
461 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
462 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
463 + wp_update_post([
464 + 'ID' => $old_special_page_id,
465 + 'post_content' => $new_content,
466 + ]);
467 + }
468 + } else {
469 + // Create special page if its missing. LP 2026-09-01
470 + $this->maybe_create_vipps_pages();
471 + }
472 + }
473 +
474 + // Admin interface for the special page on woo/advanced/pages
475 + public function woocommerce_settings_pages ($settings) {
476 + $i = -1;
477 + foreach($settings as $entry) {
478 + $i++;
479 + if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
480 + break;
481 + }
482 + }
483 + if ($i > 0) {
484 + $vippspagesettings = array(
485 + array(
486 + 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
487 + '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') ,
488 + 'id' => 'woocommerce_vipps_special_page_page_id',
489 + 'type' => 'single_select_page_with_search',
490 + 'default' => '',
491 + 'class' => 'wc-page-search',
492 + 'css' => 'min-width:300px;',
493 + 'args' => array(
494 + 'exclude' =>
495 + array(
496 + wc_get_page_id( 'myaccount' ),
497 + wc_get_page_id( 'checkout' ),
498 + wc_get_page_id( 'cart' ),
499 + ),
500 + ),
501 + 'desc_tip' => true,
502 + 'autoload' => false,
503 + ));
504 + array_splice($settings, $i, 0, $vippspagesettings);
505 + }
506 +
507 + return $settings;
508 + }
509 +
510 +
511 +
415 512 // Runs on init, adds the Vipps badge feature if activated
416 513 public function maybe_add_vipps_badge_feature () {
417 514 $badge_options = get_option('vipps_badge_options');
418 515 if (!$badge_options || !@$badge_options['badgeon']) return false;
@@ -741,8 +838,11 @@
741 838
742 839 // Get current brand and language
743 840 $current_brand = strtolower($this->get_payment_method_name());
744 841 $current_language = $this->get_customer_language();
842 + if ('se' === $current_language) $current_language = 'sv';
843 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
844 + if ('dk' === $current_language) $current_language = 'da';
745 845
746 846 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
747 847 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
748 848 'purple'=> __('Purple', 'woo-vipps')];
@@ -805,9 +905,9 @@
805 905
806 906 <h2><?php _e('Shortcodes', 'woo-vipps'); ?> </h2>
807 907 <p><?php echo sprintf(__('If you need to add a %1$s badge on a specific page, footer, header and so on, and you cannot use the Gutenberg Block provided for this, you can either add the %1$s Badge manually (as <a href="%2$s" nofollow rel=nofollow target=_blank>documented here</a>) or you can use the shortcode.', 'woo-vipps'), Vipps::CompanyName(), "https://developer.vippsmobilepay.com/docs/knowledge-base/design-guidelines/on-site-messaging/"); ?></p>
808 908 <br><?php _e("The shortcode looks like this:", 'woo-vipps')?><br>
809 - <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|dk} ] </pre><br>
909 + <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|da|sv} ] </pre><br>
810 910 <?php _e("Please refer to the documentation for the meaning of the parameters.", 'woo-vipps'); ?></br>
811 911 <?php _e("The brand will be automatically applied.", 'woo-vipps'); ?>
812 912 </p>
813 913
@@ -834,21 +934,24 @@
834 934 echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps')));
835 935 wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps'));
836 936 }
837 937
838 - $options = get_option('vipps_button_options');
839 - if (isset($_POST['express']['variant'])) {
840 - $options['express']['variant'] = sanitize_title($_POST['express']['variant']);
938 + $old = get_option('vipps_button_options2', []);
939 + $new = $old;
940 + if (isset($_POST['express']['configs'])) {
941 + foreach ($_POST['express']['configs'] as $ctx => $config) {
942 + $sanitized_ctx = sanitize_title($ctx);
943 + $sanitized_config = map_deep($config, 'sanitize_title');
944 +
945 + // If nonglobal context that uses global config, just wipe the rest of the stored config. LP 2026-06-25
946 + if ("global" !== $sanitized_ctx && ($sanitized_config['use-global-config'] ?? false)) {
947 + $new['express']['configs'][$sanitized_ctx] = ['use-global-config' => true];
948 + } else {
949 + $new['express']['configs'][$sanitized_ctx] = $sanitized_config;
950 + }
951 + }
841 952 }
842 - if (isset($_POST['express']['mini-variant'])) {
843 - $options['express']['mini-variant'] = sanitize_title($_POST['express']['mini-variant']);
844 - }
845 - if (isset($_POST['express']['force-mini']) && is_array($_POST['express']['force-mini'])) {
846 - foreach($_POST['express']['force-mini'] as $key => $val)
847 - $options['express']['force-mini'][$key] = sanitize_title($val);
848 - }
849 -
850 - update_option('vipps_button_options', $options);
953 + update_option('vipps_button_options2', $new);
851 954 wp_safe_redirect(admin_url("admin.php?page=vipps_button_menu"));
852 955 exit();
853 956 }
854 957
@@ -882,9 +985,12 @@
882 985 public function vipps_mobilepay_badge_shortcode($atts) {
883 986 $args = shortcode_atts( array('id'=>'', 'class'=>'', 'brand' => '', 'variant' => '','language'=>''), $atts );
884 987
885 988 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple', 'filled', 'light']) ? $args['variant'] : "";
886 - $language = in_array($args['language'], ['en','no', 'fi', 'dk']) ? $args['language'] : $this->get_customer_language();
989 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
990 + if ('se' === $language) $language = 'sv';
991 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
992 + if ('dk' === $language) $language = 'da';
887 993 $id = sanitize_title($args['id']);
888 994 $class = sanitize_text_field($args['class']);
889 995
890 996 $attributes = [];
@@ -900,13 +1006,18 @@
900 1006 return "<vipps-mobilepay-badge $badgeatts></vipps-mobilepay-badge>";
901 1007 }
902 1008
903 1009 // legacy vipps_badge shortcode, the new one is vipps_mobilepay_badge_shortcode. LP 19.11.2024
1010 + // Diff: this one doesn't support brand (JUST VIPPS). LP 2026-08-11
904 1011 public function vipps_badge_shortcode($atts) {
905 1012 $args = shortcode_atts( array('id'=>'', 'class'=>'','variant' => '','language'=>''), $atts );
906 1013
907 1014 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple']) ? $args['variant'] : "";
908 - $language = in_array($args['language'], ['en','no', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1015 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1016 + if ('se' === $language) $language = 'sv';
1017 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1018 + if ('dk' === $language) $language = 'da';
1019 +
909 1020 $id = sanitize_title($args['id']);
910 1021 $class = sanitize_text_field($args['class']);
911 1022
912 1023 $attributes = [];
@@ -920,163 +1031,318 @@
920 1031
921 1032 return "<vipps-badge $badgeatts></vipps-badge>";
922 1033 }
923 1034
924 - public function get_express_logo_variants() {
1035 + public function get_html_button_default_attrs() {
925 1036 return [
926 - 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
927 - 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
928 - 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
929 - 'express-pill' => __('Express pill', 'woo-vipps'),
930 - 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
931 - 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
1037 + 'language' => 'store',
1038 + 'variant' => 'primary',
1039 + 'rounded' => 'false',
1040 + 'verb' => 'buy',
1041 + 'stretched' => 'false',
1042 + 'compact' => 'false',
1043 + 'brand' => strtolower($this->get_payment_method_name()), // NB: if setting wp option, you need to remember to unset this value so it's dynamic. LP 2026-07-01
932 1044 ];
933 1045 }
934 1046
1047 + public function get_html_button_attrs_for_context($context = 'global') {
1048 + $options = get_option('vipps_button_options2', []);
1049 + if (!is_string($context)) $context = 'global';
1050 + $config = $options['express']['configs'][$context] ?? [];
1051 + if (!$config || ($config['use-global-config'] ?? false)) {
1052 + $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
1053 + }
1054 + return $config;
1055 + }
935 1056
1057 + public function get_html_button_for_context($context = 'global') {
1058 + return $this->get_html_button($this->get_html_button_attrs_for_context($context));
1059 + }
1060 +
1061 + // Generic Vipps/MobilePay button html, as of now a web component hosted locally. LP 2026-06-24
1062 + // See info and attributes at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1063 + public function get_html_button($attrs = []) {
1064 + $payment_method = $this->get_payment_method_name();
1065 + $attrs = wp_parse_args($attrs, $this->get_html_button_default_attrs());
1066 + $attrs['brand'] = strtolower($payment_method);
1067 + $attrs['type'] = 'button'; // static
1068 +
1069 + // Support using store language
1070 + if ('store' === $attrs['language']) $attrs['language'] = $this->get_customer_language();
1071 + // Don't support these login verbs. LP 2026-06-04
1072 + if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy';
1073 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1074 + if ('dk' === $attrs['language']) $attrs['language'] = 'da';
1075 +
1076 + $escaped_attrs = [];
1077 + foreach($attrs as $k => $v) {
1078 + $escaped_attrs[$k] = esc_attr($v);
1079 + }
1080 +
1081 + // id attribute
1082 + $id = $escaped_attrs['id'] ?? '';
1083 + $id_str = $id ? "id='$id'" : '';
1084 +
1085 + // class attribute
1086 + $class_str = '';
1087 + if (isset($attrs['class'])) {
1088 + if (is_array($attrs['class'])) {
1089 + $class_str = implode(' ', $attrs['class']);
1090 + } else if (is_string($attrs['class'])) {
1091 + $class_str = $attrs['class'];
1092 + }
1093 + }
1094 +
1095 + // The html
1096 + $html = <<<EOF
1097 +<vipps-mobilepay-button
1098 + $id_str
1099 + $class_str
1100 + type="{$escaped_attrs['type']}"
1101 + brand="{$escaped_attrs['brand']}"
1102 + language="{$escaped_attrs['language']}"
1103 + variant="{$escaped_attrs['variant']}"
1104 + rounded="{$escaped_attrs['rounded']}"
1105 + verb="{$escaped_attrs['verb']}"
1106 + stretched="{$escaped_attrs['stretched']}"
1107 + compact="{$escaped_attrs['compact']}"
1108 +></vipps-mobilepay-button>
1109 +EOF;
1110 + return apply_filters('woo_vipps_html_button', $html, $attrs);
1111 + }
1112 +
936 1113 public function button_menu_page() {
937 1114 if (!current_user_can('manage_woocommerce')) {
938 1115 wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
939 1116 }
940 - $payment_method = $this->get_payment_method_name();
941 - $lang = $this->get_customer_language();
942 - $button_options = get_option('vipps_button_options');
1117 + wp_enqueue_script('vipps-button-webcomponent');
1118 + ?>
1119 + <div class='wrap vipps-button-settings'>
1120 + <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1121 + <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
1122 + <form id="vipps-button-settings-form" class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1123 + <input type="hidden" name="action" value="update_vipps_button_settings" />
1124 + <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
943 1125
944 - $variants = $this->get_express_logo_variants();
945 - $mini_variants = array_filter($variants, fn($key) => str_ends_with($key, 'mini'), ARRAY_FILTER_USE_KEY);
1126 + <!-- Express section -->
1127 + <?php $this->button_menu_express_section(); ?>
946 1128
947 - $init_states = [
948 - 'express' => [
949 - 'variant' => array_key_exists(@$button_options['express']['variant'], $variants) ? $button_options['express']['variant'] : 'buy-now-rectangular',
950 - 'mini-variant' => array_key_exists(@$button_options['express']['mini-variant'], $mini_variants) ? $button_options['express']['mini-variant'] : 'express-rectangular-mini',
951 - 'force-mini' => [
952 - 'product' => @$button_options['express']['force-mini']['product'] ?? 'no',
953 - 'catalog' => @$button_options['express']['force-mini']['catalog'] ?? 'yes',
954 - 'cart' => @$button_options['express']['force-mini']['cart'] ?? 'no',
955 - 'minicart' => @$button_options['express']['force-mini']['minicart'] ?? 'no',
956 - ],
957 - ],
1129 + <!-- submit button -->
1130 + <div id="vipps-button-settings-save">
1131 + <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
1132 + </div>
1133 + </form>
1134 + </div>
1135 + <?php
1136 + }
1137 +
1138 + private function button_menu_express_section() {
1139 + $options = get_option('vipps_button_options2', []);
1140 + $express = $options['express'] ?? [];
1141 + $configs = $express['configs'] ?? [];
1142 + $contexts = [
1143 + 'global' => __('Global', 'woo-vipps'),
1144 + 'product' => __('Product', 'woo-vipps'),
1145 + 'catalog' => __('Catalog', 'woo-vipps'),
1146 + 'cart' => __('Cart', 'woo-vipps'),
1147 + 'minicart' => __('Mini cart', 'woo-vipps'),
1148 + 'checkout' => __('Checkout', 'woo-vipps'),
958 1149 ];
1150 + $init_context = 'global';
1151 + $init_config = $configs[$init_context] ?? [];
959 1152
1153 + // html button args
1154 + $init_args = $init_config;
1155 + $init_args['id'] = 'vipps-button-express-preview';
1156 +
960 1157 ?>
961 - <div class='wrap vipps-button-settings'>
962 - <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
963 - <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
964 - <form class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1158 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-container">
1159 + <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
965 1160
966 - <!-- EXPRESS SECTION -->
967 - <div id="vipps-button-settings-express-container">
968 - <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
969 - <input type="hidden" name="action" value="update_vipps_button_settings" />
970 - <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
1161 + <!-- Context dropdown -->
1162 + <div id="vipps-button-settings-express-context">
1163 + <label>
1164 + <?php _e('Config context', 'woo-vipps'); ?>
1165 + </label>
1166 + <select id="context" onChange='updateContext()'>
1167 + <?php foreach($contexts as $key => $label): ?>
1168 + <option value="<?php echo $key; ?>" <?php if ('global' === $key) echo " selected "; ?> >
1169 + <?php echo $label ; ?>
1170 + </option>
1171 + <?php endforeach; ?>
1172 + </select>
1173 + <label class="hidden" id="use-global-config-container"><input onchange="updateContext()" type="checkbox" name="express[tmpConfig][use-global-config]" checked><?php _e('Use global config', 'woo-vipps'); ?></label>
1174 + </div>
1175 +
971 1176
972 - <!-- variant -->
973 - <div class="vipps-button-settings-section">
974 - <!-- variant dropdown -->
975 - <div class="vipps-button-settings-express-demo-container">
976 - <label for="vippsButtonVariant"><?php _e('Choose variant', 'woo-vipps'); ?></label>
977 - <select id="vippsButtonVariant" name="express[variant]" onChange='changeExpressVariant()'>
978 - <?php foreach($variants as $key=>$name): ?>
979 - <option value="<?php echo $key; ?>" <?php if ($init_states['express']['variant'] === $key) echo " selected "; ?> >
980 - <?php echo $name ; ?>
981 - </option>
982 - <?php endforeach; ?>
983 - </select>
984 - </div>
1177 + <!-- Button paremeter inputs. These input values are put into post data express.tmpConfig temporarily.
1178 + On context change, configs are stored in a global 'contextConfigs'. Each config is processed into new option structure before submit. LP 2026-06-24 -->
1179 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-args">
1180 + <fieldset>
1181 + <label><input type="checkbox" name="express[tmpConfig][rounded]" checked=""><?php _e('Rounded', 'woo-vipps'); ?></label>
1182 + <label><input type="checkbox" name="express[tmpConfig][compact]"><?php _e('Compact', 'woo-vipps'); ?></label>
1183 + <label><input type="checkbox" name="express[tmpConfig][stretched]"><?php _e('Stretched', 'woo-vipps'); ?></label>
1184 + </fieldset>
1185 + <fieldset>
1186 + <legend><?php _e('Language', 'woo-vipps'); ?></legend>
1187 + <label><input type="radio" name="express[tmpConfig][language]" checked value="store"><?php _e('Store language', 'woo-vipps'); ?></label>
1188 + <label><input type="radio" name="express[tmpConfig][language]" value="en"><?php _e('English', 'woo-vipps'); ?></label>
1189 + <label><input type="radio" name="express[tmpConfig][language]" value="no"><?php _e('Norwegian', 'woo-vipps'); ?></label>
1190 + <label><input type="radio" name="express[tmpConfig][language]" value="dk"><?php _e('Danish', 'woo-vipps'); ?></label>
1191 + <label><input type="radio" name="express[tmpConfig][language]" value="sv"><?php _e('Swedish', 'woo-vipps'); ?></label>
1192 + <?php if ($this->get_payment_method_name() === 'MobilePay'): ?>
1193 + <label><input type="radio" disabled="" name="express[tmpConfig][language]" value="fi"><?php _e('Finnish', 'woo-vipps'); ?></label>
1194 + <?php endif; ?>
1195 + </fieldset>
985 1196
986 - <!-- Preload all variant images. Javascript will show the active one. LP 2025-12-16 -->
987 - <div class="vipps-button-settings-express-demo-container vipps-button-settings-img-container">
988 - <?php foreach(array_keys($variants) as $variant): ?>
989 - <img
990 - class="vipps-button-settings-express-demo"
991 - id="vipps-button-settings-express-demo-<?php echo $variant; ?>"
992 - src="<?php echo $this->get_express_logo($payment_method, $lang, $variant); ?>"
993 - style="display: <?php echo ($variant === $init_states['express']['variant'] ? 'block' : 'none') ;?>;"
994 - >
995 - <?php endforeach; ?>
996 - </div>
997 - </div>
1197 + <?php if ($this->get_payment_method_name() !== 'MobilePay'): ?>
1198 + <p><?php printf(__('Finnish is currently only available with the %s payment method.', 'woo-vipps'), 'MobilePay'); ?></p>
1199 + <?php endif; ?>
998 1200
1201 + <fieldset>
1202 + <legend><?php _e('Verb', 'woo-vipps'); ?></legend>
1203 + <label><input type="radio" name="express[tmpConfig][verb]" checked value="buy"><?php _e('Buy', 'woo-vipps'); ?></label>
1204 + <label><input type="radio" name="express[tmpConfig][verb]" value="pay"><?php _e('Pay', 'woo-vipps'); ?></label>
1205 + <label><input type="radio" name="express[tmpConfig][verb]" value="continue"><?php _e('Continue', 'woo-vipps'); ?></label>
1206 + <label><input type="radio" name="express[tmpConfig][verb]" value="confirm"><?php _e('Confirm', 'woo-vipps'); ?></label>
1207 + <label><input type="radio" name="express[tmpConfig][verb]" value="donate"><?php _e('Donate', 'woo-vipps'); ?></label>
1208 + <label><input type="radio" name="express[tmpConfig][verb]" value="express"><?php _e('Express', 'woo-vipps'); ?></label>
1209 + </fieldset>
1210 + <fieldset>
1211 + <legend><?php _e('Variant', 'woo-vipps'); ?></legend>
1212 + <label><input type="radio" name="express[tmpConfig][variant]" checked value="primary"><?php _e('Primary', 'woo-vipps'); ?></label>
1213 + <label><input type="radio" name="express[tmpConfig][variant]" value="dark"><?php _e('Dark (WCAG AAA)', 'woo-vipps'); ?></label>
1214 + <label><input type="radio" name="express[tmpConfig][variant]" value="light"><?php _e('Light (WCAG AAA)', 'woo-vipps'); ?></label>
1215 + </fieldset>
1216 + </div>
999 1217
1000 - <!-- mini variant section -->
1001 - <div class="vipps-button-settings-section">
1002 - <!-- Checkboxes "Use mini version for x page" -->
1003 - <label><?php _e('Force mini variant in these contexts:', 'woo-vipps'); ?></label>
1004 - <div class="vipps-button-settings-express-force-mini-container">
1005 - <label class="vipps-button-settings-express-force-mini" id="vipps-button-settings-express-force-mini-product"><?php _e('Product page', 'woo-vipps'); ?></label>
1006 - <input name="express[force-mini][product]" type="hidden" value="no">
1007 - <input name="express[force-mini][product]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['product'] == "yes") echo "checked";?>>
1008 - </div>
1218 + <!-- Button preview that changes depending on the chosen parameters. LP 2026-06-24 -->
1219 + <?php echo $this->get_html_button($init_args); ?>
1220 + </div>
1009 1221
1010 - <div class="vipps-button-settings-express-force-mini-container">
1011 - <label class="vipps-button-settings-express-force-mini" id="vipps-button-settings-express-force-mini-catalog"><?php _e('Catalog page', 'woo-vipps'); ?></label>
1012 - <input name="express[force-mini][catalog]" type="hidden" value="no">
1013 - <input name="express[force-mini][catalog]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['catalog'] == "yes") echo "checked";?>>
1014 - </div>
1222 + <script>
1223 + // When inputs change, update the preview args. LP 2026-06-24
1224 + jQuery('#vipps-button-settings-express-args input').on('click', updatePreview);
1015 1225
1016 - <div class="vipps-button-settings-express-force-mini-container">
1017 - <label class="vipps-button-settings-express-force-mini" id="vipps-button-settings-express-force-mini-cart"><?php _e('Cart', 'woo-vipps'); ?></label>
1018 - <input name="express[force-mini][cart]" type="hidden" value="no">
1019 - <input name="express[force-mini][cart]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['cart'] == "yes") echo "checked";?>>
1020 - </div>
1226 + let currentContext = '<?php echo $init_context; ?>';
1227 + let contextConfigs = <?php echo json_encode($configs) ?: "{}"; ?> // maps context slug to config object. LP 2026-06-24
1021 1228
1022 - <div class="vipps-button-settings-express-force-mini-container">
1023 - <label class="vipps-button-settings-express-force-mini" id="vipps-button-settings-express-force-mini-minicart"><?php _e('Mini cart', 'woo-vipps'); ?></label>
1024 - <input name="express[force-mini][minicart]" type="hidden" value="no">
1025 - <input name="express[force-mini][minicart]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['minicart'] == "yes") echo "checked";?>>
1026 - </div>
1229 + // Updates the actual html inputs from given config. LP 2026-07-01
1230 + function setInputsFromConfig(context, config) {
1231 + const useGlobalConfig = Boolean(config?.["use-global-config"]);
1232 + const isGlobal = "global" === context;
1027 1233
1028 - <!-- mini variant dropdown -->
1029 - <div class="vipps-button-settings-express-mini-demo-container">
1030 - <label for="vippsButtonMiniVariant"><?php _e('Choose variant to use in mini contexts', 'woo-vipps'); ?></label>
1031 - <select id="vippsButtonMiniVariant" name="express[mini-variant]" onChange='changeExpressMiniVariant()'>
1032 - <?php foreach($mini_variants as $key=>$name): ?>
1033 - <option value="<?php echo $key; ?>" <?php if ($init_states['express']['mini-variant'] === $key) echo " selected "; ?> >
1034 - <?php echo $name ; ?>
1035 - </option>
1036 - <?php endforeach; ?>
1037 - </select>
1038 - </div>
1234 + // Only show the 'use-global-config' checkbox for nonglobal context. LP 2026-06-26
1235 + jQuery('#use-global-config-container').toggleClass('hidden', isGlobal);
1039 1236
1040 - <!-- Preload mini variant imgs. LP 2025-12-17 -->
1041 - <div class="vipps-button-settings-express-mini-demo-container vipps-button-settings-img-container">
1042 - <?php foreach(array_keys($mini_variants) as $variant): ?>
1043 - <img
1044 - class="vipps-button-settings-express-mini-demo"
1045 - id="vipps-button-settings-express-mini-demo-<?php echo $variant; ?>"
1046 - src="<?php echo $this->get_express_logo($payment_method, $lang, $variant); ?>"
1047 - style="display: <?php echo ($variant === $init_states['express']['mini-variant'] ? 'block' : 'none') ;?>;"
1048 - >
1049 - <?php endforeach; ?>
1050 - </div>
1051 - </div>
1237 + // Nonglobal contexts with useGlobalConfig, and empty configs, should fallback to the global config. LP 2026-06-26
1238 + if (!config || (!isGlobal && useGlobalConfig)) {
1239 + config = contextConfigs["global"];
1052 1240
1053 - <!-- END EXPRESS SECTION -->
1054 - </div>
1241 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", true);
1055 1242
1056 - <!-- Save button -->
1057 - <div id="vipps-button-settings-save">
1058 - <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
1059 - </div>
1243 + // When using global config, the inputs should be disabled until its unchecked. LP 2026-06-26
1244 + jQuery('#vipps-button-settings-express-args input').prop("disabled", true);
1245 + } else {
1246 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", false);
1247 + jQuery('#vipps-button-settings-express-args input').prop("disabled", false);
1248 + }
1060 1249
1061 - </form>
1062 - </div>
1250 + Object.entries(config).forEach(([key, val]) => {
1251 + if ("use-global-config" === key) return;
1252 + const inputs = jQuery(`input[name="express[tmpConfig][${key}]"]`);
1253 + const type = inputs.prop('type');
1254 + switch (type) {
1255 + case "checkbox":
1256 + inputs.prop('checked', typeof val === "boolean" ? val : "true" === val);
1257 + break;
1258 + case "radio":
1259 + inputs.filter(`[value="${val}"]`).prop('checked', true);
1260 + break;
1261 + default:
1262 + console.error(`woo-vipps: Unexpected input type '${type}' for button config. key=${key}, val=${val}`);
1263 + }
1264 + });
1063 1265
1064 - <script>
1065 - function changeExpressVariant() {
1066 - const variant = jQuery('#vippsButtonVariant').val().trim();
1067 - // Show the one selected, hide all others. LP 2025-12-16
1068 - jQuery('.vipps-button-settings-express-demo').hide();
1069 - jQuery(`#vipps-button-settings-express-demo-${variant}`).show();
1070 - }
1266 + updatePreview();
1267 + }
1268 + // init the starting config from option. LP 2026-06-25
1269 + setInputsFromConfig(currentContext, contextConfigs[currentContext]);
1071 1270
1072 - function changeExpressMiniVariant() {
1073 - const variant = jQuery('#vippsButtonMiniVariant').val().trim();
1074 - // Show the one selected, hide all others. LP 2025-12-16
1075 - jQuery('.vipps-button-settings-express-mini-demo').hide();
1076 - jQuery(`#vipps-button-settings-express-mini-demo-${variant}`).show();
1077 - }
1078 - </script>
1271 + // Update the preview web component's attributes. LP 2026-06-24
1272 + function updatePreview(event) {
1273 + const args = getPreviewArgs();
1274 + // 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
1275 + // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1276 + if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1277 + const button = jQuery('#vipps-button-express-preview');
1278 + button.attr(args);
1279 + }
1280 +
1281 + function getPreviewArgs() {
1282 + const args = {};
1283 + jQuery('#vipps-button-settings-express-args input').each(function () {
1284 + // inputs are put in form arrays like 'express[tmpConfig][attribute]', so extract the actual attribute name. LP 2026-06-24
1285 + const matches = [...this.name.matchAll(/\[([^\]]+)\]/g)];
1286 + const attr = matches.length ? matches[matches.length - 1][1] : null;
1287 + if (!attr) {
1288 + console.error("woo-vipps: Could not extract attribute name for button preview:", this);
1289 + return;
1290 + }
1291 + if (this.type === 'checkbox') {
1292 + args[attr] = this.checked;
1293 + } else if (this.checked) {
1294 + args[attr] = this.value;
1295 + }
1296 + });
1297 +
1298 + return args;
1299 + }
1300 +
1301 + // Stores selected config for context and switches to another (if changed). LP 2026-07-01
1302 + function updateContext() {
1303 + const wasGlobal = "global" === currentContext;
1304 + const useGlobalConfig = jQuery('#use-global-config-container input').prop("checked");
1305 +
1306 + // Store config to global, unless its a non-global context that uses global config. LP 2026-06-25
1307 + if (wasGlobal || !useGlobalConfig) {
1308 + contextConfigs[currentContext] = getPreviewArgs();
1309 + } else {
1310 + contextConfigs[currentContext] = {'use-global-config': true};
1311 + }
1312 +
1313 + // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1314 + const newContext = jQuery("#context").val();
1315 + const newConfig = contextConfigs[newContext];
1316 + setInputsFromConfig(newContext, newConfig);
1317 + currentContext = newContext;
1318 + }
1319 +
1320 + // Before submit: delete the tmpConfig for the current selected values, and add the stored contextConfigs to the post data. LP 2026-06-24
1321 + jQuery('#vipps-button-settings-form').on('formdata', e => {
1322 + const formData = e?.originalEvent?.formData;
1323 + if (!formData) return;
1324 +
1325 + // run this to store current context config before posting. LP 2026-06-24
1326 + updateContext();
1327 +
1328 + // now we can delete the current temporary config from post data. LP 2026-06-24
1329 + const keysToDelete = [];
1330 + for (const [key] of formData.entries()) {
1331 + if (key.startsWith("express[tmpConfig][")) {
1332 + keysToDelete.push(key);
1333 + }
1334 + }
1335 + keysToDelete.forEach(key => formData.delete(key));
1336 +
1337 + // Now add the actual post data from the stored global contextConfigs. LP 2026-06-24
1338 + Object.entries(contextConfigs).forEach(([context, config]) => {
1339 + Object.entries(config).forEach(([key, val]) => {
1340 + formData.append(`express[configs][${context}][${key}]`, val);
1341 + });
1342 + });
1343 + });
1344 + </script>
1079 1345 <?php
1080 1346 }
1081 1347
1082 1348
@@ -1329,22 +1595,35 @@
1329 1595 public function delete_old_cancelled_orders() {
1330 1596 $limit = 30;
1331 1597 $cutoff = time() - 600; // Ten minutes old orders: Delete them
1332 1598 $oldorders = time() - (60*60*24*7); // Very old orders: Ignore them to make this work on sites with enormous order databases
1333 - // Ensure the old order table understands the meta query IOK 2022-12-02
1334 - static::add_wc_order_meta_key_support();
1335 - $args = array(
1599 + $delenda = [];
1600 +
1601 + if ($this->useHPOS()) {
1602 + $args = array(
1336 1603 'status' => 'cancelled',
1337 1604 'limit' => $limit,
1338 1605 'date_modified' => "$oldorders...$cutoff",
1339 - 'meta_vipps_delendum' => 1);
1340 - if ($this->useHPOS()) {
1341 - /* The above, with the filter, is for the old orders table, the below is for the new IOK 2022-12-02 */
1342 - $args['meta_query'] = [[ 'key' => '_vipps_delendum', 'value' => 1 ]];
1606 + 'meta_query' => [[ 'key' => '_vipps_delendum', 'value' => 1 ]]
1607 + );
1608 + $delenda = wc_get_orders($args);
1609 + } else {
1610 + // Old-style orders, we'll just use SQL
1611 + global $wpdb;
1612 + $sql = $wpdb->prepare("SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm on (pm.post_id = p.ID AND pm.meta_key = '_vipps_delendum') WHERE p.post_type = 'shop_order' AND p.post_status = 'wc-cancelled' AND p.post_modified_gmt >= %s AND p.post_modified_gmt <= %s AND pm.meta_value = 1 LIMIT %d",
1613 + gmdate( 'Y-m-d H:i:s', $oldorders ),
1614 + gmdate( 'Y-m-d H:i:s', $cutoff ),
1615 + $limit
1616 + );
1617 + $order_ids = $wpdb->get_col($sql);
1618 + foreach($order_ids as $did) {
1619 + $d = wc_get_order($did);
1620 + if ($d && !is_wp_error($d)) {
1621 + $delenda[] = $d;
1622 + }
1623 + }
1343 1624 }
1344 1625
1345 - $delenda = wc_get_orders($args);
1346 -
1347 1626 foreach ($delenda as $del) {
1348 1627 // Delete only if there is no customer info for the order IOK 2022-10-12
1349 1628 if (!$del->get_billing_email()) {
1350 1629 $del->delete(true);
@@ -1460,9 +1739,9 @@
1460 1739 $orderid = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0;
1461 1740 $order = wc_get_order($orderid);
1462 1741 }
1463 1742 }
1464 - if (is_a($order, 'WC_Order') && $order->get_payment_method() == 'vipps') {
1743 + if (is_a($order, 'WC_Order') && self::is_vipps_order($order)) {
1465 1744 $vippsorder = true;
1466 1745 }
1467 1746
1468 1747 if ($vippsorder) {
@@ -1478,20 +1757,37 @@
1478 1757 }
1479 1758 wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/vipps.js"), 'true');
1480 1759
1481 1760 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1482 - wp_register_script('vipps-onsite-messageing','https://checkout.vipps.no/on-site-messaging/v1/vipps-osm.js',array(),WOO_VIPPS_VERSION,
1483 - array(
1761 + wp_register_script('vipps-onsite-messageing',
1762 + plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1763 + array(),
1764 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1765 + [
1484 1766 'in_footer' => true,
1485 1767 'strategy' => 'async',
1486 - ));
1768 + ],
1769 + );
1487 1770
1771 + // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1772 + wp_register_script('vipps-button-webcomponent',
1773 + plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1774 + array(),
1775 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1776 + [
1777 + 'in_footer' => false
1778 + ],
1779 + );
1488 1780 }
1489 1781
1490 1782 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1491 1783 public function script_add_vippslocale () {
1492 1784 // This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13
1493 - $strings = array('Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()),'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()));
1785 + $strings = array(
1786 + 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1787 + 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()),
1788 + 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1789 + );
1494 1790 wp_localize_script('vipps-gw', 'VippsLocale', $strings);
1495 1791 }
1496 1792
1497 1793 public function wp_enqueue_scripts() {
@@ -1500,8 +1796,9 @@
1500 1796 $this->script_add_vippslocale();
1501 1797
1502 1798 wp_enqueue_script('vipps-gw');
1503 1799 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1800 + wp_enqueue_script('vipps-button-webcomponent');
1504 1801 }
1505 1802
1506 1803
1507 1804 public function add_shortcodes() {
@@ -1513,8 +1810,11 @@
1513 1810 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1514 1811 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1515 1812 // Legacy vipps-badge shortcode. LP 19.11.2024
1516 1813 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1814 +
1815 + // special page handling, previously a fake page. LP 2026-08-25
1816 + add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1517 1817 }
1518 1818
1519 1819
1520 1820 public function log ($what,$type='info') {
@@ -1540,8 +1840,10 @@
1540 1840 }
1541 1841
1542 1842 // Show express button option on checkout form. LP 2026-03-23
1543 1843 public function checkout_before_customer_details_express () {
1844 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1845 + do_action('woo_vipps_checkout_before_customer_details_express');
1544 1846 $gw = $this->gateway();
1545 1847 if (!$gw->show_express_checkout()) return;
1546 1848 $this->express_checkout_section_html();
1547 1849 }
@@ -1548,13 +1850,13 @@
1548 1850
1549 1851 public function express_checkout_section_html() {
1550 1852 $payment_method = $this->get_payment_method_name();
1551 1853 $header_text = __('Express Checkout', 'woo-vipps');
1552 - $header = "<div class='express-header'>$header_text</div>";
1854 + $header = "<legend class='express-header'>$header_text</legend>";
1553 1855 $div_classes = "legacy-checkout vipps-express-checkout $payment_method";
1554 - echo "<div class='$div_classes'>$header";
1555 - $this->cart_express_checkout_button_html();
1556 - echo '</div>';
1856 + echo "<fieldset class='$div_classes'>$header";
1857 + $this->checkout_express_checkout_button_html();
1858 + echo '</fieldset>';
1557 1859 }
1558 1860
1559 1861 public function express_checkout_banner() {
1560 1862 $gw = $this->gateway();
@@ -1580,8 +1882,27 @@
1580 1882 <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1581 1883 <?php
1582 1884 }
1583 1885
1886 + public function checkout_express_checkout_button() {
1887 + $gw = $this->gateway();
1888 +
1889 + if ($gw->show_express_checkout()){
1890 + return $this->checkout_express_checkout_button_html();
1891 + }
1892 + }
1893 +
1894 + public function checkout_express_checkout_button_html() {
1895 + $url = $this->express_checkout_url();
1896 + $url = wp_nonce_url($url,'express','sec');
1897 + $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1898 + $method = $this->get_payment_method_name();
1899 + $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1900 + $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
1901 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1902 + echo $html;
1903 + }
1904 +
1584 1905 // Show the express button if reasonable to do so
1585 1906 public function cart_express_checkout_button() {
1586 1907 $gw = $this->gateway();
1587 1908
@@ -1600,22 +1921,46 @@
1600 1921
1601 1922 public function cart_express_checkout_button_html($minicart = false) {
1602 1923 $url = $this->express_checkout_url();
1603 1924 $url = wp_nonce_url($url,'express','sec');
1604 - $page = $minicart ? 'minicart' : 'cart';
1605 - $imgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo($page));
1925 + $context = $minicart ? 'minicart' : 'cart';
1926 + $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1606 1927 $method = $this->get_payment_method_name();
1607 1928 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1608 - $button = "<a href='$url' class='button vipps-express-checkout short $method' title='$title'><img alt='$title' border=0 src='$imgurl'></a>";
1609 - $button = apply_filters('woo_vipps_cart_express_checkout_button', $button, $url);
1610 - echo $button;
1929 + $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
1930 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1931 + echo $html;
1611 1932 }
1612 1933
1613 1934 // A shortcode for a single buy now button. Express checkout must be active; but I don't check for this here, as this button may be
1614 1935 // cached. Therefore stock, purchasability etc will be done later. IOK 2018-10-02
1615 1936 public function buy_now_button_shortcode ($atts) {
1616 - $args = shortcode_atts( array( 'id' => '','variant'=>'','sku' => '',), $atts );
1617 - return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode') . "</div>";
1937 + // The new web component button args. LP 2026-07-02
1938 + $button_args = $this->get_html_button_default_attrs();
1939 + unset($button_args['brand']);
1940 +
1941 + // Variant exists for the product variant. LP 2026-07-02
1942 + if (isset($button_args['variant'])) $button_args['button_variant'] = $button_args['variant'];
1943 + unset($button_args['variant']);
1944 +
1945 + $args = shortcode_atts(
1946 + array(...$button_args,
1947 + 'id' => '','variant'=> '','sku' => '',
1948 + ),
1949 + $atts,
1950 + );
1951 +
1952 + // Variant exists for the product variant. LP 2026-07-02
1953 + $button_args = $args;
1954 + if (isset($button_args['button_variant'])) $button_args['variant'] = $button_args['button_variant'];
1955 + unset($button_args['button_variant']);
1956 + unset($button_args['sku']);
1957 + unset($button_args['id']);
1958 + // NB: the language may be incorrect for the shortcode, see web component bug at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1959 + // "Note also that there is a bug in the library, and it currently only renders one language per page."
1960 + // it seems like get_html_button() runs once before this shortcode code (whic gets default attrs including language), so I think this is why language bug appears. LP 2026-07-02
1961 +
1962 + return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode', $button_args) . "</div>";
1618 1963 }
1619 1964
1620 1965 // The express checkout shortcode implementation. It does not need to check if we are to show the button, obviously, but needs to see if the cart works
1621 1966 public function express_checkout_button_shortcode() {
@@ -1891,9 +2236,9 @@
1891 2236 public function add_vipps_metabox ($post_or_order_object) {
1892 2237 $order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object;
1893 2238 $order = wc_get_order($post_or_order_object);
1894 2239 $pm = $order->get_payment_method();
1895 - if ($pm != 'vipps') return;
2240 + if (!self::is_vipps_order($pm)) return;
1896 2241 $orderid=$order->get_id();
1897 2242
1898 2243 $init = intval($order->get_meta('_vipps_init_timestamp'));
1899 2244 $callback = intval($order->get_meta('_vipps_callback_timestamp'));
@@ -1989,9 +2334,9 @@
1989 2334 print "<p>" . __("Unknown order", 'woo-vipps') . "</p>";
1990 2335 exit();
1991 2336 }
1992 2337 $pm = $order->get_payment_method();
1993 - if ($pm != 'vipps') {
2338 + if (!self::is_vipps_order($pm)) {
1994 2339 print "<p>" . sprintf(__("The order is not a %1\$s order", 'woo-vipps'), $this->get_payment_method_name()) . "</p>";
1995 2340 exit();
1996 2341 }
1997 2342
@@ -2235,24 +2580,28 @@
2235 2580
2236 2581
2237 2582 // Because the prefix used to create the Vipps order id is editable
2238 2583 // by the user, we will store that as a meta and use this for callbacks etc.
2239 - // IOK: This needs to be replaced by a separate table, but in the meantime, we will use
2240 - // wc_get_orders and not $wpdb directly, so it should work with HPOS too.
2241 2584 // IOK 2023-01-23 this function is no longer used, and kept only for backwards compatibility with
2242 2585 // debug filters and similar.
2586 + // IOK 2026-05-27 rewritten to avoid wc_get_orders for pre-HPOS. Still not used.
2243 2587 public function getOrderIdByVippsOrderId($vippsorderid) {
2244 - // Ensure the old order table understands the meta query IOK 2022-12-02
2245 - static::add_wc_order_meta_key_support();
2246 - $result = wc_get_orders( array(
2247 - 'limit' => 1,
2248 - 'return' => 'ids',
2249 - 'meta_vipps_orderid' => $vippsorderid,
2250 - /* The above, with the filter, is for the old orders table, the below is for the new IOK 2022-12-02 */
2251 - 'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]]
2252 - ));
2253 - if ($result && is_array($result)) return $result[0];
2254 -
2588 + $result = false;
2589 + if ($this->useHPOS()) {
2590 + $result = wc_get_orders( array(
2591 + 'limit' => 1,
2592 + 'return' => 'ids',
2593 + 'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]]
2594 + ));
2595 + if ($result && is_array($result)) return $result[0];
2596 + } else {
2597 + // Pre-HPOS did not support meta_query, so we're doing it with direct access to the database. IOK 2026-05-27
2598 + global $wpdb;
2599 + $q = $wpdb->prepare("SELECT p.ID from `{$wpdb->posts}` p JOIN `{$wpdb->postmeta}` m ON (m.post_id = p.ID and m.meta_key = '_vipps_orderid') WHERE p.post_type = 'shop_order' AND m.meta_value = %s LIMIT 1", $vippsorderid);
2600 + $res = $wpdb->get_results($q, ARRAY_A);
2601 + if (empty($res)) return 0;
2602 + return $res[0]['ID'];
2603 + }
2255 2604 return 0;
2256 2605 }
2257 2606
2258 2607 // This is like getOrderByVipsOrderId, but only fetches pending orders.
@@ -2263,9 +2612,8 @@
2263 2612 $result = wc_get_orders( array(
2264 2613 'limit' => 1,
2265 2614 'status' => 'wc-pending',
2266 2615 'type' => 'shop_order',
2267 - 'payment_method' => 'vipps',
2268 2616 'date_created' => '>' . $sevendaysago,
2269 2617 'return' => 'objects',
2270 2618 'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]]
2271 2619 ));
@@ -2281,77 +2629,69 @@
2281 2629 return null;
2282 2630 }
2283 2631 }
2284 2632
2633 + // Special pages, and some callbacks. IOK 2018-05-18
2634 + public function template_redirect() {
2285 2635
2286 - // If this is a special page, return true very early because we are handling this. IOK 2023-02-22
2287 - public function pre_handle_404($current, $query) {
2288 - if (!is_admin()) {
2289 - $special = $this->is_special_page();
2290 - if ($special) {
2291 - // Ensure very early on that Autooptimize does not try to optimize us (if installed) IOK 2023-03-04
2292 - add_filter( 'autoptimize_filter_noptimize', '__return_true');
2293 - return true;
2294 - }
2636 + // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2637 + // We redirect these to the new location.
2638 + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2639 + if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2640 + $url = static::get_special_page_url();
2641 + $_GET['action'] = 'buy_product';
2642 + $q = build_query($_GET);
2643 + wp_redirect($url . "?" . $q, 302);
2644 + exit();
2295 2645 }
2296 - return $current;
2297 - }
2298 2646
2299 - // Special pages, and some callbacks. IOK 2018-05-18
2300 - public function template_redirect() {
2301 - global $post;
2302 - // Handle special callbacks
2303 - $special = $this->is_special_page() ;
2304 -
2305 - if ($special) {
2647 + if (static::is_special_page()) {
2648 + // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2306 2649 remove_filter('template_redirect', 'redirect_canonical', 10);
2307 - do_action('woo_vipps_before_handling_special_page', $special);
2650 + // dont cache special page. LP 2026-08-25
2651 + $this->nocache();
2652 + // Do the custom pre-load actions for these pages IOK 2026-09-11
2653 + do_action('woo_vipps_before_handling_special_page', $_GET['action']);
2654 + }
2655 + }
2308 2656
2309 - // Allow above hook to actually handle special pages. It should probably call $Vipps->fakepage or a redirect; can be used
2310 - // to intercept express checkout etc. IOK 2022-03-18
2311 - if (! apply_filters('woo_vipps_special_page_handled', false, $special)) {
2312 - $this->$special();
2313 - }
2314 - }
2657 + // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2658 + public function vipps_special_page_endpoint_title($title, $postid = 0) {
2659 + global $wp_query;
2660 + // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2315 2661
2316 - $consentremoval = $this->is_consent_removal();
2317 - if ($consentremoval) {
2318 - remove_filter('template_redirect', 'redirect_canonical', 10);
2319 - do_action('woo_vipps_before_handling_special_page', 'consentremoval');
2320 - if (! apply_filters('woo_vipps_special_page_handled', false, 'consentremoval')) {
2321 - $this->vipps_consent_removal_callback($consentremoval);
2662 + // In block themes the whole template (header, footer, content) renders inside the main
2663 + // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2664 + // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2665 + // the queried page so an earlier title doesn't consume this one-shot filter.
2666 + if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2667 + switch ($_GET['action'] ?? '') {
2668 + case 'wait_for_payment':
2669 + $title = __('Processing order', 'woo-vipps');
2670 + break;
2671 + case 'do_express_checkout':
2672 + case 'buy_product':
2673 + $title = __('Express Checkout', 'woo-vipps');
2674 + break;
2322 2675 }
2323 2676 }
2677 + return $title;
2324 2678 }
2679 +
2325 2680 // Template handling for special pages. IOK 2018-11-21
2681 + // 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
2326 2682 public function template_include($template) {
2327 - $special = $this->is_special_page() ;
2328 - if ($special) {
2683 + if (static::is_special_page()) {
2329 2684 // Get any special template override from the options IOK 2020-02-18
2330 2685 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2331 2686 $found = locate_template($specific,false,false);
2332 2687 if ($found) $template=$found;
2333 2688
2334 - return apply_filters('woo_vipps_special_page_template', $template, $special);
2689 + return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2335 2690 }
2336 2691 return $template;
2337 2692 }
2338 2693
2339 -
2340 - // Can't use wc-api for this, as that does not support DELETE . IOK 2018-05-18
2341 - private function is_consent_removal () {
2342 -
2343 - if ($_SERVER['REQUEST_METHOD'] != 'DELETE') return false;
2344 - if ( !get_option('permalink_structure')) {
2345 - if (@$_REQUEST['vipps-consent-removal']) return @$_REQUEST['callback'];
2346 - return false;
2347 - }
2348 - if (preg_match("!/vipps-consent-removal/([^/]*)!", $_SERVER['REQUEST_URI'], $matches)) {
2349 - return @$_REQUEST['callback'];
2350 - }
2351 - return false;
2352 - }
2353 -
2354 2694 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2355 2695 // the user if using Express Checkout IOK 2020-10-09
2356 2696 public function woocommerce_before_thankyou ($orderid) {
2357 2697 $order = wc_get_order($orderid);
@@ -2356,9 +2696,9 @@
2356 2696 public function woocommerce_before_thankyou ($orderid) {
2357 2697 $order = wc_get_order($orderid);
2358 2698 if ($order) {
2359 2699 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2360 - // -- 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
2700 + // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2361 2701 $this->maybe_log_in_user($order);
2362 2702 $order->delete_meta_data('_vipps_limited_session');
2363 2703 $order->save();
2364 2704
@@ -2390,9 +2730,9 @@
2390 2730
2391 2731 // If local pickup has been added to express/checkout by filters, add this to emails/confirmation pages. IOK 2025-08-15
2392 2732 add_filter('woocommerce_order_shipping_to_display', function($shipping, $order, $tax_display) {
2393 2733 if (!is_a($order, 'WC_Order')) return $shipping;
2394 - if ($order->get_payment_method() != 'vipps') return $shipping;
2734 + if (! self::is_vipps_order($order)) return $shipping;
2395 2735 $shipping_method = current( $order->get_shipping_methods() );
2396 2736
2397 2737 if (empty($shipping_method)) return $shipping;
2398 2738
@@ -2419,9 +2759,8 @@
2419 2759
2420 2760 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2421 2761 // IOK 2025-11-19
2422 2762 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2423 -
2424 2763 }
2425 2764
2426 2765 public function get_payment_method_name() {
2427 2766 return $this->gateway()->get_option('payment_method_name');
@@ -2441,14 +2780,13 @@
2441 2780 public function after_setup_theme() {
2442 2781 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2443 2782 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2444 2783
2445 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2784 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2446 2785 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2447 2786 // is important.
2448 2787 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2449 2788
2450 -
2451 2789 // Callbacks use the Woo API IOK 2018-05-18
2452 2790 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2453 2791 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2454 2792
@@ -2459,9 +2797,9 @@
2459 2797 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2460 2798 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2461 2799
2462 2800 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2463 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2801 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2464 2802 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2465 2803
2466 2804 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2467 2805 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
@@ -2466,12 +2804,10 @@
2466 2804 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2467 2805 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2468 2806
2469 2807
2470 - // Special pages and callbacks handled by template_redirect
2471 - // We must also notify WP and other plugins that we are handling this 404-like situation. IOK 2023-02-22
2808 + // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2472 2809 add_action('template_redirect', array($this,'template_redirect'),1);
2473 - add_action('pre_handle_404', array($this, 'pre_handle_404'), 1, 2);
2474 2810
2475 2811 // Allow overriding their templates
2476 2812 add_filter('template_include', array($this,'template_include'), 10, 1);
2477 2813
@@ -2501,12 +2837,12 @@
2501 2837 // If we can't cancel for some other reason, don't.
2502 2838 if (!$cancel) return $cancel;
2503 2839
2504 2840 // Only check Vipps orders
2505 - if ($order->get_payment_method() != 'vipps') return $cancel;
2841 + if (! self::is_vipps_order($order)) return $cancel;
2506 2842
2507 - // For Vipps, all unpaid orders must be pending. IOK FIXME ADD FAILED
2508 - if ($order->get_status() != 'pending') return $cancel;
2843 + // For Vipps, all unpaid orders must be pending.
2844 + if ($order->get_status() != 'pending' && $order->get_status() != 'failed') return $cancel;
2509 2845
2510 2846 // Handle this separately, in the Checkout class. IOK 2025-10-08
2511 2847 $checkout_session = $order->get_meta('_vipps_checkout_session');
2512 2848 if ($checkout_session) {
@@ -2578,15 +2914,21 @@
2578 2914 $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());
2579 2915 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2580 2916 $this->vippsJSConfig['vippslocale'] = get_locale();
2581 2917 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
2582 - $this->vippsJSConfig['logoSvgUrl'] = $this->get_payment_logo('buy-now-block');
2583 2918
2584 2919
2585 2920 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2586 2921 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
2922 + // Ensure gateways are loaded at this point IOK 2026-05-27
2923 + require_once(dirname(__FILE__) . '/WC_Gateway_VippsCard.class.php');
2924 + require_once(dirname(__FILE__) . '/WC_Gateway_Vipps.class.php');
2925 +
2926 + // Then the payment blocks
2587 2927 require_once(dirname(__FILE__) . "/Blocks/Payment/Vipps.class.php");
2928 + require_once(dirname(__FILE__) . "/Blocks/Payment/VippsCard.class.php");
2588 2929 Automattic\WooCommerce\Blocks\Payments\Integrations\Vipps::register();
2930 + Automattic\WooCommerce\Blocks\Payments\Integrations\VippsCard::register();
2589 2931 }
2590 2932
2591 2933 // Used for e.g. labels of product/shipping metadata. IOK 2025-05-07
2592 2934 add_filter('woocommerce_attribute_label', function ($label, $name, $product) {
@@ -2642,9 +2984,9 @@
2642 2984 static::set_locale_if_in_header();
2643 2985 $order = wc_get_order(intval($_REQUEST['orderid']));
2644 2986 if (!is_a($order, 'WC_Order')) return;
2645 2987 $pm = $order->get_payment_method();
2646 - if ($pm != 'vipps') return;
2988 + if (!self::is_vipps_order($pm)) return;
2647 2989
2648 2990 $action = isset($_REQUEST['do']) ? sanitize_title($_REQUEST['do']) : 'none';
2649 2991
2650 2992 if ($action == 'do_capture') {
@@ -2713,9 +3055,9 @@
2713 3055 }
2714 3056
2715 3057 public function order_item_add_capture_button ($order) {
2716 3058 $pm = $order->get_payment_method();
2717 - if ($pm != 'vipps') return;
3059 + if (!self::is_vipps_order($pm)) return;
2718 3060 $status = $order->get_status();
2719 3061
2720 3062 $show_capture_button = ($status == 'on-hold' || $status == 'processing');
2721 3063 if (!apply_filters('woo_vipps_show_capture_button', $show_capture_button, $order)) {
@@ -2722,10 +3064,11 @@
2722 3064 return;
2723 3065 }
2724 3066
2725 3067 $captured = intval($order->get_meta('_vipps_captured'));
2726 - $capremain = intval($order->get_meta('_vipps_capture_remaining'));
2727 - if ($captured && !$capremain || $capremain < 2) {
3068 + // noncapturable should never be greater than capture remaining, so this *should* not be negative. LP 2026-06-12
3069 + $capremain = intval($order->get_meta('_vipps_capture_remaining')) - intval($order->get_meta('_vipps_noncapturable'));
3070 + if ($captured && (!$capremain || $capremain < 2)) {
2728 3071 print "<div><strong>" . sprintf(__("The entire amount has been captured at %1\$s", 'woo-vipps'), $this->get_payment_method_name()) . "</strong></div>";
2729 3072 return;
2730 3073 }
2731 3074
@@ -2750,14 +3093,14 @@
2750 3093
2751 3094 $raw_post = @file_get_contents( 'php://input' );
2752 3095 $result = @json_decode($raw_post,true);
2753 3096
2754 - // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
3097 + // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
2755 3098 // .. and the epayment webhooks 2023-12-19
2756 3099 $ischeckout = false;
2757 3100 $iswebhook = false;
2758 3101 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
2759 - // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
3102 + // For Checkout v3 and onwards, we control the callback so the type is just this field
2760 3103 if ($callback == 'checkout') {
2761 3104 $ischeckout = true;
2762 3105 }
2763 3106 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
@@ -3151,9 +3494,9 @@
3151 3494 print "Unknown order";
3152 3495 $this->log(__('Could not find Woo order with id:', 'woo-vipps') . " " . $orderid, 'error');
3153 3496 exit();
3154 3497 }
3155 - if ($order->get_payment_method() != 'vipps') {
3498 + if (!self::is_vipps_order($order)) {
3156 3499 status_header(400, "Invalid order");
3157 3500 print "Invalid order";
3158 3501 $this->log(__('Invalid order for shipping callback:', 'woo-vipps') . " " . $orderid, 'error');
3159 3502 exit();
@@ -3171,10 +3514,10 @@
3171 3514 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3172 3515 exit();
3173 3516 }
3174 3517
3175 - // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3176 - // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3518 + // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3519 + // special support for Checkout that this is in fact happening. IOK 2023-01-19
3177 3520 // This needs to be done before "calculate totals".
3178 3521 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3179 3522 $ischeckout = $order->get_meta('_vipps_checkout');
3180 3523
@@ -3350,9 +3693,9 @@
3350 3693 ), 'debug');
3351 3694
3352 3695 }
3353 3696
3354 - // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3697 + // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3355 3698 // 'dynamic pricing' 2023-01-26
3356 3699 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3357 3700 $taxrate = 0;
3358 3701 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
@@ -3478,9 +3821,9 @@
3478 3821 $vippsmethod['shippingMethod'] = $rate->get_label();
3479 3822 $vippsmethod['shippingMethodId'] = $key;
3480 3823 $vippsmethods[]=$vippsmethod;
3481 3824
3482 - // Metadata and settings stored for later use for Vipps Checkout
3825 + // Metadata and settings stored for later use for Checkout
3483 3826 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3484 3827 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3485 3828 // finalize the order. IOK 2025-08-15
3486 3829 $ratemap[$key]=$rate;
@@ -3498,9 +3841,9 @@
3498 3841 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3499 3842 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3500 3843 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3501 3844
3502 - // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3845 + // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3503 3846 // IOK 2025-08-15 and new Express yet another slightly different format.
3504 3847 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3505 3848 if ($ischeckout) {
3506 3849 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
@@ -3876,19 +4219,11 @@
3876 4219 header("X-Accel-Expires: 0");
3877 4220 }
3878 4221
3879 4222
3880 -
3881 - // Handle DELETE on a vipps consent removal callback
3882 - public function vipps_consent_removal_callback ($callback) {
3883 - Vipps::nocache();
3884 - // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
3885 - // to escape here when the API is changed. IOK 2020-10-14
3886 - $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
3887 - }
3888 -
3889 4223 public function woocommerce_payment_gateways($methods) {
3890 4224 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4225 + require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
3891 4226 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
3892 4227 $gateway = $this->gateway();
3893 4228 if ($gateway) {
3894 4229 $methods[] = $gateway;
@@ -3894,8 +4229,11 @@
3894 4229 $methods[] = $gateway;
3895 4230 } else {
3896 4231 $methods[] = 'WC_Gateway_Vipps';
3897 4232 }
4233 +
4234 + $methods[] = 'WC_Gateway_VippsCard';
4235 +
3898 4236 return $methods;
3899 4237 }
3900 4238
3901 4239 // Runs after set_session, so if the session is just created, we'll get called. IOK 2018-06-06
@@ -3918,9 +4256,9 @@
3918 4256 // But for express checkout this breaks because there is no shipping method or address, and of course,
3919 4257 // the order id is unique too.. IOK 2018-11-21
3920 4258 public function woocommerce_my_account_my_orders_actions($actions, $order ) {
3921 4259 $pm = $order->get_payment_method();
3922 - if ($pm != 'vipps') return $actions;
4260 + if (!self::is_vipps_order($pm)) return $actions;
3923 4261
3924 4262 if (!static::order_is_vipps_retryable($order->get_id())) {
3925 4263 unset($actions['pay']);
3926 4264 }
@@ -3938,10 +4276,14 @@
3938 4276 // to the store and the Vipps callback fails for whatever reason. IOK 2021-06-21
3939 4277 public function cron_check_for_missing_callbacks() {
3940 4278 $eightminutesago = time() - (60*8);
3941 4279 $sevendaysago = time() - (60*60*24*7);
3942 - $pending = wc_get_orders(
3943 - array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps', 'date_created' => '>' . $sevendaysago ));
4280 +
4281 + // This is compatible with both HPOS and old style order management. IOK 2026-05-27
4282 + $pending_app = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps', 'date_created' => '>' . $sevendaysago ));
4283 + $pending_cards = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps_card', 'date_created' => '>' . $sevendaysago ));
4284 + $pending = array_merge($pending_app, $pending_cards);
4285 +
3944 4286 if (empty($pending)) return;
3945 4287 foreach ($pending as $o) {
3946 4288 $then = $o->get_meta('_vipps_init_timestamp');
3947 4289 if (! $then) continue; # Race condition! We may not have set the timestamp yet. IOK 2022-03-24
@@ -3953,9 +4295,9 @@
3953 4295 if ($currentstatus != 'initiated') {
3954 4296 $this->log(sprintf(__("Order %2\$d is 'pending' but its %1\$s order status is '%3\$s' - this means that the order has been erroneously set to 'pending' after completion or cancellation. Will not process further, please check status of order at %1\$s and set to correct status in WooCommerce", 'woo-vipps'), $this->get_payment_method_name(), $o->get_id(), $currentstatus), 'debug');
3955 4297 return;
3956 4298 }
3957 - $this->check_status_of_pending_order($o, false, false);
4299 + $this->check_status_of_pending_order($o, false);
3958 4300 }
3959 4301 }
3960 4302
3961 4303 // Check and possibly update the status of a pending order at Vipps. We only restore session if we know this is called from a context with no session -
@@ -3960,30 +4302,35 @@
3960 4302
3961 4303 // Check and possibly update the status of a pending order at Vipps. We only restore session if we know this is called from a context with no session -
3962 4304 // e.g. wp-cron. IOK 2021-06-21
3963 4305 // Stop restoring session in wp-cron too. IOK 2021-08-23
3964 - public function check_status_of_pending_order($order, $maybe_restore_session=0, $allow_retry=true) {
3965 - $express = $order->get_meta('_vipps_express_checkout');
3966 - $vippstatus = $order->get_meta('_vipps_status');
3967 - if ($express && $maybe_restore_session) {
3968 - $this->log(sprintf(__("Restoring session of order %1\$d", 'woo-vipps'), $order->get_id()), 'debug');
3969 - $this->callback_restore_session($order->get_id());
3970 - }
4306 + // Stop restoring session in wp-cron again(?) since we now use a rest endpoint to handle shipping. LP 2026-05-13
4307 + public function check_status_of_pending_order($order, $allow_retry=true) {
3971 4308 $gw = $this->gateway();
3972 4309
3973 4310 $order_status = null;
3974 4311 try {
3975 4312 $order->add_order_note(sprintf(__("Callback from %1\$s delayed or never happened; order status checked by periodic job", 'woo-vipps'), $this->get_payment_method_name()));
3976 - $order_status = $gw->callback_check_order_status($order, $allow_retry);
4313 +
4314 + // Poll status and correct woo status. LP 2026-05-19
4315 + $order_data = $gw->get_payment_details($order);
4316 +
4317 + // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4318 + if ('CANCEL' === ($order_data['state'] ?? "")) {
4319 + /* translators: company name */
4320 + $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4321 + return;
4322 + }
4323 +
4324 + $gw->set_order_status_by_payment_details($order, $order_data, $allow_retry);
4325 + $order = wc_get_order($order->get_id()); // refresh order if changed. LP 2026-05-13
4326 + $order_status = $order->get_status();
4327 +
3977 4328 $this->log(sprintf(__("For order %2\$d order status at %1\$s is %3\$s", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id(), $order_status), 'debug');
3978 4329 } catch (Exception $e) {
3979 4330 $this->log(sprintf(__("Error getting order status at %1\$s for order %2\$d", 'woo-vipps'), $this->get_payment_method_name(), $order->get_id()), 'error');
3980 4331 $this->log($e->getMessage() . "\n" . $order->get_id(), 'error');
3981 4332 }
3982 - // Ensure we don't keep using an old session for more than one order here.
3983 - if ($express && $maybe_restore_session) {
3984 - $this->callback_destroy_session();
3985 - }
3986 4333 return $order_status;
3987 4334 }
3988 4335
3989 4336 // This will probably be run in activate, but if the plugin is updated in other ways, will also be run on after_setup_theme. IOK 2020-04-01
@@ -3996,20 +4343,40 @@
3996 4343 }
3997 4344 }
3998 4345
3999 4346 public function activate () {
4000 - static::maybe_add_cron_event();
4001 - $gw = $this->gateway();
4347 + static::maybe_add_cron_event();
4348 + $gw = $this->gateway();
4002 4349
4003 - // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4004 - if ($gw->get_option('orderprefix') == 'Woo') {
4005 - $gw->update_option('orderprefix', $this->generate_order_prefix());
4006 - }
4007 - // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4008 - $gw->initialize_webhooks();
4009 - $this->payment_method_name = $gw->get_option('payment_method_name');
4010 - }
4350 + // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4351 + if ($gw->get_option('orderprefix') == 'Woo') {
4352 + $gw->update_option('orderprefix', $this->generate_order_prefix());
4353 + }
4354 + // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4355 + $gw->initialize_webhooks();
4356 + $this->payment_method_name = $gw->get_option('payment_method_name');
4011 4357
4358 +
4359 + // Check if the special page is noted and actually does exist
4360 + $special = static::get_special_page_id();
4361 + if ($special) {
4362 + $special_page = get_post($special);
4363 + if ($special_page && 'trash' !== $special_page->post_status) {
4364 + // Ensure this page has the necessary shortcode. LP 2026-09-01
4365 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4366 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4367 + wp_update_post([
4368 + 'ID' => $special,
4369 + 'post_content' => $new_content,
4370 + ]);
4371 + }
4372 + } else {
4373 + delete_option('woocommerce_vipps_special_page_page_id');
4374 + }
4375 + }
4376 +
4377 + }
4378 +
4012 4379 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4013 4380 public static function deactivate() {
4014 4381 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4015 4382 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
@@ -4020,13 +4387,12 @@
4020 4387 $gw->delete_all_webhooks();
4021 4388
4022 4389 // Delete all settings if checked in settings menu. LP 2025-10-06
4023 4390 $should_delete = $gw->get_option( 'delete_settings_on_deactivation' ) === 'yes';
4024 - if ( ($should_delete)) {
4391 + if ($should_delete) {
4025 4392 // Delete options.
4026 - $options = ['woocommerce_vipps_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4393 + $options = ['woocommerce_vipps_settings', 'woocommerce_vipps_card_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', 'vipps_button_options2', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4027 4394 foreach($options as $option) {
4028 - error_log("Deleting woo-vipps option: $option");
4029 4395 delete_option($option);
4030 4396 }
4031 4397 }
4032 4398
@@ -4063,9 +4429,10 @@
4063 4429 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4064 4430 private function maybe_set_vipps_as_default() {
4065 4431 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4066 4432 $gw = $this->gateway();
4067 - if ($gw->get_option('vippsdefault')=='yes') {
4433 + // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4434 + if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4068 4435 WC()->session->set('chosen_payment_method', $gw->id);
4069 4436 }
4070 4437 }
4071 4438
@@ -4167,16 +4534,16 @@
4167 4534 // Maybe log in user
4168 4535 // It is done on the thank-you page of the order, and only for express checkout.
4169 4536 function maybe_log_in_user ($order) {
4170 4537 if (is_user_logged_in()) return;
4171 - if (!$order || $order->get_payment_method()!= 'vipps' ) return;
4538 + if (!$order || ! self::is_vipps_order($order)) return;
4172 4539
4173 4540 // We *do* want to log in express checkout customers, but not those that
4174 - // use the Vipps Checkout solution - those can change their emails in the
4541 + // use the Checkout solution - those can change their emails in the
4175 4542 // checkout screen. IOK 2021-09-03
4176 4543 $do_login = $order->get_meta('_vipps_express_checkout');
4177 4544
4178 - // We will not log in Vipps Checkout users unless the option for that is true
4545 + // We will not log in Checkout users unless the option for that is true
4179 4546 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4180 4547 $do_login = false;
4181 4548 }
4182 4549
@@ -4204,9 +4571,9 @@
4204 4571
4205 4572 // Get the customer that corresponds to the current order, maybe creating the customer if it does not exist yet and
4206 4573 // the settings allow it.
4207 4574 function express_checkout_get_vipps_customer($order) {
4208 - if (!$order || $order->get_payment_method() != 'vipps' ) return null;
4575 + if (!$order || ! self::is_vipps_order($order)) return null;
4209 4576 // specific code for this by netthandelsgruppen if the below function exists
4210 4577 if (function_exists('create_assign_user_on_vipps_callback')) return null;
4211 4578
4212 4579 // Both Checkout and Express Checkout have the below value set to true
@@ -4211,9 +4578,9 @@
4211 4578
4212 4579 // Both Checkout and Express Checkout have the below value set to true
4213 4580 if (!$order->get_meta('_vipps_express_checkout')) return;
4214 4581
4215 - // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4582 + // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4216 4583 // IOK 2023-07-27
4217 4584 $ischeckout = $order->get_meta('_vipps_checkout');
4218 4585 if ($ischeckout) {
4219 4586 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
@@ -4598,9 +4965,9 @@
4598 4965 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4599 4966 }
4600 4967 }
4601 4968
4602 - // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4969 + // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4603 4970 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4604 4971 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4605 4972 // 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
4606 4973 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
@@ -4671,46 +5038,37 @@
4671 5038 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4672 5039 return false;
4673 5040 }
4674 5041
4675 - // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4676 - // These are for the "fallback URL" mostly. IOK 2018-05-18
4677 - private function make_vipps_url($what) {
4678 - if ( !get_option('permalink_structure')) {
4679 - return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4680 - }
4681 - return trailingslashit(home_url($what, 'https'));
5042 + // 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
5043 + private function make_special_page_url($action) {
5044 + return add_query_arg('action', $action, $this->get_special_page_url());
4682 5045 }
5046 +
4683 5047 public function payment_return_url() {
4684 - return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
5048 + return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
4685 5049 }
4686 5050 public function express_checkout_url() {
4687 - return $this->make_vipps_url('vipps-express-checkout');
5051 + return $this->make_special_page_url('do_express_checkout');
4688 5052 }
4689 5053 public function buy_product_url() {
4690 - return $this->make_vipps_url('vipps-buy-product');
5054 + return $this->make_special_page_url('buy_product');
4691 5055 }
4692 5056
4693 - // Return the method in the Vipps
4694 - public function is_special_page() {
4695 - $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4696 - $method = null;
4697 - if ( get_option('permalink_structure')) {
4698 - foreach($specials as $special=>$specialmethod) {
4699 - // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4700 - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4701 - if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4702 - $method = $specialmethod; break;
4703 - }
4704 - }
4705 - } else {
4706 - if (isset($_GET['VippsSpecialPage'])) {
4707 - $method = @$specials[$_GET['VippsSpecialPage']];
4708 - }
4709 - }
4710 - return $method;
5057 + public static function is_special_page() {
5058 + $id = static::get_special_page_id();
5059 + return $id && is_page($id);
4711 5060 }
4712 5061
5062 + public static function get_special_page_id() {
5063 + $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5064 + return $id > 0 ? $id : null;
5065 + }
5066 +
5067 + public static function get_special_page_url() {
5068 + return get_permalink(static::get_special_page_id());
5069 + }
5070 +
4713 5071 // Just create a spinner and a overlay.
4714 5072 public function spinner () {
4715 5073 $flavour = sanitize_title($this->get_payment_method_name());
4716 5074 ob_start();
@@ -4731,164 +5089,22 @@
4731 5089 return apply_filters('woo_vipps_spinner', ob_get_clean());
4732 5090 }
4733 5091
4734 5092
4735 - // Returns express logo images depending on parameters, these are the new express svgs received 2025-12-12.
4736 - // Fallbacks to defaults for each payment method. LP 2025-12-15
4737 - public function get_express_logo($payment_method, $lang, $variant) {
4738 - $base = plugins_url('img', __FILE__);
4739 -
4740 - // A much more concise approach could be to name the variant files directly and do a oneliner, but harder to grep after the files' usage. LP 2025-12-12
4741 - $img_map = [
4742 - "vipps" => [
4743 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4744 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4745 - "en" => [
4746 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4747 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4748 - "buy-now-rectangular" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4749 - "buy-now-pill" => "$base/vipps/express/en/buy-now-vipps-en-pill.svg",
4750 - "express-rectangular" => "$base/vipps/express/en/express-vipps-en-rectangular.svg",
4751 - "express-rectangular-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4752 - "express-pill" => "$base/vipps/express/en/express-vipps-en-pill.svg",
4753 - "express-pill-mini" => "$base/vipps/express/en/express-vipps-en-pill-mini.svg",
4754 -
4755 - ],
4756 - "no" => [
4757 - "default" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4758 - "default-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4759 - "buy-now-rectangular" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4760 - "buy-now-pill" => "$base/vipps/express/no/kjop-na-vipps-no-pill.svg",
4761 - "express-rectangular" => "$base/vipps/express/no/ekspress-vipps-no-rectangular.svg",
4762 - "express-rectangular-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4763 - "express-pill" => "$base/vipps/express/no/ekspress-vipps-no-pill.svg",
4764 - "express-pill-mini" => "$base/vipps/express/no/ekspress-vipps-no-pill-mini.svg",
4765 - ],
4766 - "se" => [
4767 - "default" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4768 - "default-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4769 - "buy-now-rectangular" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4770 - "buy-now-pill" => "$base/vipps/express/se/kop-nu-vipps-se-pill.svg",
4771 - "express-rectangular" => "$base/vipps/express/se/express-vipps-se-rectangular.svg",
4772 - "express-rectangular-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4773 - "express-pill" => "$base/vipps/express/se/express-vipps-se-pill.svg",
4774 - "express-pill-mini" => "$base/vipps/express/se/express-vipps-se-pill-mini.svg",
4775 -
4776 - ],
4777 - ],
4778 - "mobilepay" => [
4779 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4780 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4781 - "en" => [
4782 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4783 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4784 - "buy-now-rectangular" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4785 - "buy-now-pill" => "$base/mobilepay/express/en/buy-now-mp-en-pill.svg",
4786 - "express-rectangular" => "$base/mobilepay/express/en/express-mp-en-rectangular.svg",
4787 - "express-rectangular-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4788 - "express-pill" => "$base/mobilepay/express/en/express-mp-en-pill.svg",
4789 - "express-pill-mini" => "$base/mobilepay/express/en/express-mp-en-pill-mini.svg",
4790 -
4791 - ],
4792 - "dk" => [
4793 - "default" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4794 - "default-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4795 - "buy-now-rectangular" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4796 - "buy-now-pill" => "$base/mobilepay/express/dk/kob-nu-mp-dk-pill.svg",
4797 - "express-rectangular" => "$base/mobilepay/express/dk/express-mp-dk-rectangular.svg",
4798 - "express-rectangular-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4799 - "express-pill" => "$base/mobilepay/express/dk/express-mp-dk-pill.svg",
4800 - "express-pill-mini" => "$base/mobilepay/express/dk/express-mp-dk-pill-mini.svg",
4801 - ],
4802 - "fi" => [
4803 - "default" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4804 - "default-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4805 - "buy-now-rectangular" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4806 - "buy-now-pill" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-pill.svg",
4807 - "express-rectangular" => "$base/mobilepay/express/fi/express-mp-fi-rectangular.svg",
4808 - "express-rectangular-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4809 - "express-pill" => "$base/mobilepay/express/fi/express-mp-fi-pill.svg",
4810 - "express-pill-mini" => "$base/mobilepay/express/fi/express-mp-fi-pill-mini.svg",
4811 -
4812 - ],
4813 - ],
4814 -
4815 - ];
4816 -
4817 - $payment = strtolower($payment_method);
4818 - if ($lang === 'store') $lang = $this->get_customer_language();
4819 -
4820 - // Dont give a default if payment method not found. LP 2025-12-12
4821 - if (!array_key_exists($payment, $img_map)) {
4822 - return null;
4823 - }
4824 - $payment_map = $img_map[$payment];
4825 -
4826 - $img = null;
4827 - if (array_key_exists($lang, $payment_map)
4828 - && is_array($payment_map[$lang])
4829 - && array_key_exists($variant, $payment_map[$lang])) {
4830 - $img = @$payment_map[$lang][$variant];
4831 - }
4832 -
4833 - // Default fallback behaviour
4834 - if (!$img) {
4835 - $default = str_ends_with($variant, '-mini') ? 'default-mini' : 'default';
4836 -
4837 - // First try getting default for payment method + language. LP 2026-01-16
4838 - if (array_key_exists($lang, $payment_map) && is_array($payment_map[$lang])) {
4839 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4840 - $this->log(sprintf(__('Could not find chosen express logo for payment method %1$s, language %2$s, and variant %3$s, attempting to fall back on language and payment method, else only language.', 'woo-vipps'), $payment_method, $lang, $variant), 'error');
4841 - $img = @$payment_map[$lang][$default];
4842 - }
4843 -
4844 - // If not found, then try global default for payment method. LP 2026-01-16
4845 - if (!$img) {
4846 - $img = @$payment_map[$default];
4847 - }
4848 -
4849 - // Found no logo at all, log this. LP 2026-01-16
4850 - if (!$img) {
4851 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4852 - $this->log(sprintf(__('Found no express logo fallback for payment method %1$s, language %2$s, and variant %3$s.', 'woo-vipps'), $payment_method, $lang, $variant), 'error');
4853 - }
4854 - }
4855 - return $img;
5093 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
5094 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5095 + public function get_express_logo($_payment_method = null, $_lang = null, $_variant = null, $context = 'global') {
5096 + return $this->get_html_button_for_context($context);
4856 5097 }
4857 5098
5099 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
4858 5100 // Get payment logo based on payment method, then language NT 2023-11-30
4859 - // and based on custom variant setting. $page is the page origin slug, e.g 'cart', 'product'. LP 2025-12-15
4860 - public function get_payment_logo($page = null) {
4861 - $lang = $this->get_customer_language();
4862 - $payment_method = $this->get_payment_method_name();
4863 - $variant = $this->get_express_logo_page_variant($page);
4864 - $logo_url = $this->get_express_logo($payment_method, $lang, $variant);
4865 - return $logo_url;
5101 + // and based on custom variant setting. $context is where it is to be used, e.g 'cart', 'product'. LP 2025-12-15
5102 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5103 + public function get_payment_logo($context = 'global') {
5104 + return $this->get_express_logo(null, null, null, $context);
4866 5105 }
4867 5106
4868 - /** Returns the correct variant to use for the given page, found from the wp option. LP 2025-12-23 */
4869 - private function get_express_logo_page_variant($page = null) {
4870 - $options = get_option('vipps_button_options');
4871 -
4872 - // Init defaults, use mini version by default in below pages. LP 2025-12-17
4873 - $use_mini = in_array($page, ['catalog']);
4874 - $variant = "";
4875 -
4876 - // Find correct variant from button settings. LP 2025-12-17
4877 - if (is_array($options) && array_key_exists('express', $options)) {
4878 - if (array_key_exists($page, $options['express']['force-mini'])) {
4879 - $use_mini = sanitize_title($options['express']['force-mini'][$page]) === 'yes';
4880 - }
4881 - $key = $use_mini ? 'mini-variant' : 'variant';
4882 - $variant = sanitize_title($options['express'][$key]) ?? '';
4883 - }
4884 -
4885 - if (!$variant) {
4886 - $variant = $use_mini ? "default-mini" : "default";
4887 - }
4888 - return apply_filters('woo_vipps_express_button_page_variant', $variant, $page);
4889 - }
4890 -
4891 5107 // Get express banner logo based on payment method. LP 2025-09-03
4892 5108 private function get_express_banner_logo() {
4893 5109 $payment_method = $this->get_payment_method_name();
4894 5110
@@ -4899,10 +5115,12 @@
4899 5115 }
4900 5116 return null;
4901 5117 }
4902 5118
4903 - // Get buy now button by manually selecting logo variant and language. LP 2026-01-16
4904 - public function get_buy_now_button_manual($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $logo_variant=null, $logo_lang=null) {
5119 + // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5120 + // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5121 + // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5122 + public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
4905 5123 $disabled = $disabled ? 'disabled' : '';
4906 5124 $data = array();
4907 5125
4908 5126 // Support directly using the variant id as $product_id with no $variation_id. LP 2026-01-23
@@ -4917,9 +5135,8 @@
4917 5135 if ($sku) $data['product_sku'] = $sku;
4918 5136 if ($product_id) $data['product_id'] = $product_id;
4919 5137 if ($variation_id) $data['variation_id'] = $variation_id;
4920 5138
4921 -
4922 5139 $buttoncode = "<a href='javascript:void(0)' $disabled ";
4923 5140 foreach($data as $key=>$value) {
4924 5141 $value = esc_attr($value);
4925 5142 $buttoncode .= " data-$key='$value' ";
@@ -4926,14 +5143,18 @@
4926 5143 }
4927 5144
4928 5145 $payment_method = $this->get_payment_method_name();
4929 5146 $title = sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method);
4930 - $short = str_ends_with($logo_variant, 'mini');
4931 - $logo = $this->get_express_logo($payment_method, $logo_lang, $logo_variant);
4932 5147
4933 - $message =" <img border=0 src='$logo' alt='$payment_method'/>";
5148 + if (is_array($button_args_override) && $button_args_override) {
5149 + $button_args = $button_args_override;
5150 + } else {
5151 + $button_args = $this->get_html_button_attrs_for_context($context);
5152 + }
5153 + $short = ($button_args['compact'] ?? 'false') === 'true';
5154 + $button = $this->get_html_button($button_args);
4934 5155
4935 -# Extra classes, if passed IOK 2019-02-26
5156 + # Extra classes, if passed IOK 2019-02-26
4936 5157 if (is_array($classes)) {
4937 5158 $classes = join(" ", $classes);
4938 5159 }
4939 5160 if ($classes) $classes = " $classes";
@@ -4938,19 +5159,15 @@
4938 5159 }
4939 5160 if ($classes) $classes = " $classes";
4940 5161 if ($short) $classes = "short $classes";
4941 5162
4942 - $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$message</a>";
5163 + $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5164 +
5165 +
5166 +
4943 5167 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
4944 5168 }
4945 5169
4946 - // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
4947 - public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $page=null) {
4948 - $logo_lang = $this->get_customer_language();
4949 - $logo_variant = $this->get_express_logo_page_variant($page);
4950 - return $this->get_buy_now_button_manual($product_id, $variation_id, $sku, $disabled, $classes, $logo_variant, $logo_lang);
4951 - }
4952 -
4953 5170 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
4954 5171 public function single_product_buy_now_button () {
4955 5172 $gw = $this->gateway();
4956 5173 $how = $gw->get_option('singleproductexpress');
@@ -5031,43 +5248,90 @@
5031 5248 }
5032 5249
5033 5250
5034 5251
5035 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5252 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5036 5253 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5254 + // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5037 5255 public function woocommerce_create_pages ($data) {
5256 + // Vipps Checkout page
5038 5257 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5039 - if (!$vipps_checkout_activated) return $data;
5258 + if ($vipps_checkout_activated) {
5259 + $data['vipps_checkout'] = array(
5260 + 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5261 + 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5262 + 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5263 + );
5264 + }
5040 5265
5041 - $data['vipps_checkout'] = array(
5042 - 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5043 - 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5044 - 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5045 - );
5046 -
5266 + // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5267 + $data['vipps_special_page'] = [
5268 + 'name' => 'vipps-payment', // slug
5269 + /* translators: company name */
5270 + 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5271 + 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5272 + ];
5047 5273 return $data;
5048 5274 }
5049 5275
5050 - // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5276 + // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5277 + // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5051 5278 public function maybe_create_vipps_pages () {
5279 + $make_pages = false;
5280 +
5281 + // Vipps Checkout page. LP 2026-08-18
5052 5282 $checkoutid = wc_get_page_id('vipps_checkout');
5053 - $makeit = !$checkoutid || ! get_post_status($checkoutid);
5054 - if ($makeit) {
5283 + if (!$checkoutid || ! get_post_status($checkoutid)) {
5055 5284 delete_option('woocommerce_vipps_checkout_page_id');
5285 + $make_pages = true;
5056 5286 }
5057 5287
5058 - if ($makeit) {
5059 - WC_Install::create_pages();
5288 + // vipps special page, previously a fake page. LP 2026-08-18
5289 + $builtin_special_page_id = static::get_special_page_id();
5290 + if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5291 + delete_option('woocommerce_vipps_special_page_page_id');
5292 + $make_pages = true;
5060 5293 }
5294 +
5295 + if ($make_pages) {
5296 + WC_Install::create_pages();
5297 + }
5061 5298 }
5062 5299
5300 + public function vipps_special_page_shortcode($atts, $content) {
5301 + // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5302 + if (is_admin()) return;
5303 + if (wp_doing_ajax()) return;
5304 + if (defined('REST_REQUEST') && REST_REQUEST) return;
5305 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5063 5306
5307 + $action = $_GET['action'] ?? '';
5308 + $html = "";
5309 + switch ($action) {
5310 + case 'wait_for_payment':
5311 + $html = $this->vipps_wait_for_payment();
5312 + break;
5313 + case 'do_express_checkout':
5314 + $html = $this->vipps_express_checkout();
5315 + break;
5316 + case 'buy_product':
5317 + $html = $this->vipps_buy_product();
5318 + break;
5319 + default:
5320 + $html = '';
5321 + }
5322 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5323 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5324 +
5325 + // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5326 + return $html;
5327 + }
5328 +
5329 +
5064 5330 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5065 5331 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5066 5332 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5067 5333 public function vipps_buy_product() {
5068 - status_header(200,'OK');
5069 - Vipps::nocache();
5070 5334
5071 5335 add_filter('body_class', function ($classes) {
5072 5336 $classes[] = 'vipps-express-checkout';
5073 5337 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
@@ -5103,9 +5367,9 @@
5103 5367
5104 5368 if (!$productinfo) {
5105 5369 $title = __("Product is no longer available",'woo-vipps');
5106 5370 $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');
5107 - return $this->fakepage($title,$content);
5371 + return $this->special_page_html($title,$content);
5108 5372 }
5109 5373
5110 5374 // Pass the productinfo to the express checkout form
5111 5375 $args = array();
@@ -5122,15 +5386,13 @@
5122 5386 }
5123 5387 $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5124 5388 }
5125 5389
5126 - $this->print_express_checkout_page(true,'do_single_product_express_checkout',$args);
5390 + return $this->express_checkout_page_html(true,'do_single_product_express_checkout',$args);
5127 5391 }
5128 5392
5129 5393 // This is a landing page for the express checkout of then normal cart - it is done like this because this could take time on slower hosts.
5130 5394 public function vipps_express_checkout() {
5131 - status_header(200,'OK');
5132 - Vipps::nocache();
5133 5395 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5134 5396 // IOK 2018-05-28
5135 5397 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5136 5398
@@ -5157,9 +5419,9 @@
5157 5419 });
5158 5420
5159 5421 do_action('woo_vipps_express_checkout_page');
5160 5422
5161 - $this->print_express_checkout_page(true, 'do_express_checkout');
5423 + return $this->express_checkout_page_html(true, 'do_express_checkout');
5162 5424 }
5163 5425
5164 5426 // This method tries to ensure that a customer does not 'lose' the return page and
5165 5427 // starts ordering the same products twice. IOK 2020-01-22
@@ -5254,9 +5516,10 @@
5254 5516 return $orderspec;
5255 5517 }
5256 5518
5257 5519 // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5258 - protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5520 + // Returns the html. LP 2026-08-27
5521 + protected function express_checkout_page_html($execute,$action,$productinfo=null) {
5259 5522 $gw = $this->gateway();
5260 5523
5261 5524 $expressCheckoutMessages = array();
5262 5525 $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
@@ -5267,11 +5530,12 @@
5267 5530 wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5268 5531 wp_enqueue_script('vipps-express-checkout');
5269 5532 // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5270 5533 // to actually perform the express checkout.
5271 - $buttonimgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo('landing'));
5534 + $buttonhtml = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button());
5272 5535
5273 5536
5537 +
5274 5538 $orderspec = $this->get_orderspec_from_arguments($productinfo);
5275 5539 if (empty($orderspec)) {
5276 5540 $orderspec = $this->get_orderspec_from_cart();
5277 5541 }
@@ -5338,10 +5602,9 @@
5338 5602
5339 5603 if ($execute) {
5340 5604 $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5341 5605 $content .= "<div id='vipps-status-message'></div>";
5342 - $this->fakepage(__('Order in progress','woo-vipps'), $content);
5343 - return;
5606 + return $this->special_page_html('', $content);
5344 5607 } else {
5345 5608 $content .= $askForConfirmationHTML;
5346 5609 $content .= $extraHTML;
5347 5610 $content .= $termsHTML;
@@ -5346,12 +5609,11 @@
5346 5609 $content .= $extraHTML;
5347 5610 $content .= $termsHTML;
5348 5611 $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5349 5612 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5350 - $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='button vipps-express-checkout' title='$title'><img alt='$title' border=0 src='$buttonimgurl'></a></div>";
5613 + $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>";
5351 5614 $content .= "<div id='vipps-status-message'></div>";
5352 - $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5353 - return;
5615 + return $this->special_page_html('', $content);
5354 5616 }
5355 5617 }
5356 5618
5357 5619
@@ -5356,11 +5618,8 @@
5356 5618
5357 5619
5358 5620
5359 5621 public function vipps_wait_for_payment() {
5360 - status_header(200,'OK');
5361 - Vipps::nocache();
5362 -
5363 5622 $orderid = WC()->session->get('_vipps_pending_order');
5364 5623
5365 5624 $order = null;
5366 5625 $gw = $this->gateway();
@@ -5395,9 +5654,8 @@
5395 5654 $session->set('_vipps_pending_order', $orderid);
5396 5655 }
5397 5656 }
5398 5657
5399 -
5400 5658 do_action('woo_vipps_wait_for_payment_page',$order);
5401 5659
5402 5660 $deleted_order=0;
5403 5661 if ($orderid && !$order) {
@@ -5461,11 +5719,9 @@
5461 5719 }
5462 5720 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5463 5721 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5464 5722 $content .= "</div>";
5465 - $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5466 -
5467 - return;
5723 + return $this->special_page_html('', $content);
5468 5724 }
5469 5725
5470 5726 // Still pending and order is supposed to exist, so wait for Vipps. This happens all the time, so logging is removed. IOK 2018-09-27
5471 5727
@@ -5471,14 +5727,8 @@
5471 5727
5472 5728 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5473 5729 wp_enqueue_script('check-vipps',plugins_url('js/check-order-status.js',__FILE__),array('jquery','vipps-gw'),filemtime(dirname(__FILE__) . "/js/check-order-status.js"), 'true');
5474 5730
5475 - // 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
5476 - // and check that the order matches (and is 'pending') (and exists)
5477 - $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5478 - $vippsstatus = $order->get_meta('_vipps_status');
5479 - $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5480 -
5481 5731 $signal = $this->callbackSignal($order);
5482 5732 $content = "";
5483 5733 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5484 5734
@@ -5511,94 +5761,22 @@
5511 5761 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5512 5762 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5513 5763 $content .= "</div>";
5514 5764
5765 + return $this->special_page_html('', $content);
5766 + }
5515 5767
5516 - $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5768 + // Returns formatted html for the vipps special page. LP 2026-08-27
5769 + public function special_page_html($header, $content) {
5770 + $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5771 + $html = <<<EOF
5772 + $header_html
5773 + <div class="vipps-special-page-content">$content</div>
5774 + EOF;
5775 + return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5517 5776 }
5518 5777
5519 5778
5520 -
5521 - public function fakepage($title,$content) {
5522 - global $wp, $wp_query;
5523 - // We don't want this here.
5524 - remove_filter ('the_content', 'wpautop');
5525 -
5526 - $specialid = $this->gateway()->get_option('vippsspecialpageid');
5527 - $wp_post = null;
5528 - if ($specialid) {
5529 - $wp_post = get_post($specialid);
5530 - if ($wp_post) {
5531 - $wp_post->post_title = $title;
5532 - $wp_post->post_content = $content;
5533 - // Normalize a bit
5534 - $wp_post->filter = 'raw'; // important
5535 - $wp_post->post_status = 'publish';
5536 - $wp_post->comment_status= 'closed';
5537 - $wp_post->ping_status= 'closed';
5538 - } else {
5539 - $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5540 - }
5541 - }
5542 - if (!$wp_post || is_wp_error($wp_post)) {
5543 - $post = new stdClass();
5544 - $post->ID = -99;
5545 - $post->post_author = 1;
5546 - $post->post_date = current_time( 'mysql' );
5547 - $post->post_date_gmt = current_time( 'mysql', 1 );
5548 - $post->post_title = $title;
5549 - $post->post_content = $content;
5550 - $post->post_status = 'publish';
5551 - $post->comment_status = 'closed';
5552 - $post->ping_status = 'closed';
5553 - $post->post_name = 'vippsconfirm-fake-page-name';
5554 - $post->post_type = 'page';
5555 - $post->filter = 'raw'; // important
5556 - $wp_post = new WP_Post($post);
5557 - wp_cache_add( -99, $wp_post, 'posts' );
5558 - }
5559 -
5560 - // Update the main query
5561 - $wp_query->post = $wp_post;
5562 - $wp_query->posts = array( $wp_post );
5563 - $wp_query->queried_object = $wp_post;
5564 - $wp_query->queried_object_id = $wp_post->ID;
5565 - $wp_query->found_posts = 1;
5566 - $wp_query->post_count = 1;
5567 - $wp_query->max_num_pages = 1;
5568 - $wp_query->is_page = true;
5569 - $wp_query->is_singular = true;
5570 - $wp_query->is_single = false;
5571 - $wp_query->is_attachment = false;
5572 - $wp_query->is_archive = false;
5573 - $wp_query->is_category = false;
5574 - $wp_query->is_tag = false;
5575 - $wp_query->is_tax = false;
5576 - $wp_query->is_author = false;
5577 - $wp_query->is_date = false;
5578 - $wp_query->is_year = false;
5579 - $wp_query->is_month = false;
5580 - $wp_query->is_day = false;
5581 - $wp_query->is_time = false;
5582 - $wp_query->is_search = false;
5583 - $wp_query->is_feed = false;
5584 - $wp_query->is_comment_feed = false;
5585 - $wp_query->is_trackback = false;
5586 - $wp_query->is_home = false;
5587 - $wp_query->is_embed = false;
5588 - $wp_query->is_404 = false;
5589 - $wp_query->is_paged = false;
5590 - $wp_query->is_admin = false;
5591 - $wp_query->is_preview = false;
5592 - $wp_query->is_robots = false;
5593 - $wp_query->is_posts_page = false;
5594 - $wp_query->is_post_type_archive = false;
5595 - // Update globals
5596 - $GLOBALS['wp_query'] = $wp_query;
5597 - $wp->register_globals();
5598 - return $wp_post;
5599 - }
5600 -
5601 5779 // Support the interactivity API with data about our cart IOK 2026-02-23
5602 5780 public function woo_vipps_store_api_cart_data() {
5603 5781 // 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
5604 5782
@@ -5604,13 +5782,15 @@
5604 5782
5605 5783 $checkout_page = $this->gateway()->vipps_checkout_available();
5606 5784 $standard_checkout = get_permalink(get_option('woocommerce_checkout_page_id'));
5607 5785 $checkout_url = $checkout_page ? get_permalink($checkout_page) : $standard_checkout;
5786 +
5608 5787 $cart_data = array(
5609 5788 'cart_hide_express' => !$this->gateway()->show_express_checkout(),
5610 5789 'cart_supports_checkout' => (bool) $checkout_page,
5611 5790 'checkout_url' => $checkout_url,
5612 5791 );
5792 +
5613 5793 return $cart_data;
5614 5794 }
5615 5795
5616 5796 public function woo_vipps_store_api_cart_schema() {
@@ -5632,8 +5812,117 @@
5632 5812 ),
5633 5813 );
5634 5814 }
5635 5815
5816 + // Inits option 'vipps_button_options' and handles migration from older versions. LP 2026-06-26
5817 + // new version is stored as vipps_button_options2 to avoid breaking older versions on version revert. IOK 2026-07-15
5818 + private function init_button_options() {
5819 + /* New structure as of now
5820 + * [
5821 + * 'version' => x.x,
5822 + * 'express' => [
5823 + * 'version' => x.x, used to migrate from previous iterations
5824 + * 'configs' => [ different button parameters for certain contexts, falls back to global if context has no override
5825 + * 'global' => ['compact' => ..., 'verb' => ..., ...],
5826 + * 'cart' => [...],
5827 + * 'product' => [...],
5828 + * 'checkout' => [...],
5829 + * ...
5830 + * ],
5831 + * 'product_configs' => [ overrides for specific products
5832 + * 1532 => ['compact' => ..., 'verb' => ..., ...],
5833 + * ...
5834 + * ],
5835 + * ],
5836 + * ]
5837 + */
5838 + $options = get_option('vipps_button_options2');
5839 + if (!empty($options)) return;
5840 +
5841 + $old_options = get_option('vipps_button_options');
5842 + $default_config = $this->get_html_button_default_attrs();
5843 + unset($default_config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5844 + $default_compact = array_replace($default_config, ['compact' => 'true']);
5845 +
5846 + $default_options = [
5847 + 'version' => $this->button_options_version,
5848 + 'express' => [
5849 + 'version' => $this->button_options_express_version,
5850 + 'configs' => [
5851 + 'global' => $default_config,
5852 + // Need compact version by default for below pages. LP 2026-07-07
5853 + 'catalog' => $default_compact,
5854 + 'minicart' => $default_compact, // storefront needs compact, tho 2025 theme has a lot of room. Just use compact LP 2026-07-07
5855 + ],
5856 + 'product_configs' => [],
5857 + ],
5858 + ];
5859 +
5860 + $new_options = $default_options;
5861 +
5862 + //Actually, we have some options from the old structure IOK 2026-07-15
5863 + if (!empty($old_options)) {
5864 + $new_options['express']['configs']['global'] = $this->migrate_button_variant_to_config($old_options['express']['variant'] ?? '');
5865 + unset($new_options['express']['configs']['global']['brand']); // dont set brand, this needs to be dynamic. LP 2026-07-01
5866 + }
5867 +
5868 + // Migrate context/page mini override to new context config. LP 2026-06-26
5869 + if (is_array($old_options['express']['force-mini'] ?? null)) {
5870 + foreach($old_options['express']['force-mini'] as $context => $use_mini) {
5871 + if ("yes" === $use_mini) {
5872 + $config = $this->migrate_button_variant_to_config($old_options['express']['mini-variant'] ?? '');
5873 + unset($config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5874 + $config['compact'] = 'true';
5875 + $new_options['express']['configs'][$context] = $config;
5876 + }
5877 + }
5878 + }
5879 +
5880 + if ($this->get_payment_method_name() !== 'MobilePay') {
5881 + // Finnish is only available in the MobilePay component right now, so reset language in any configs. LP 2026-07-01
5882 + foreach(($new_options['express']['configs'] ?? []) as $context => $config) {
5883 + if ('fi' === ($config['language'] ?? '')) {
5884 + $config['language'] = 'store';
5885 + $new_options['express']['configs'][$context] = $config;
5886 + }
5887 + }
5888 + }
5889 +
5890 + /* translators: placeholders are arrays */
5891 + $this->log(sprintf(__('Migrating from old button options. Old: %s, new: %s', 'woo-vipps'), print_r($options, true), print_r($new_options, true)), 'debug');
5892 +
5893 +
5894 +
5895 + update_option('vipps_button_options2', $new_options);
5896 + }
5897 +
5898 + // Old variant string => new config array. LP 2026-06-26
5899 + public function migrate_button_variant_to_config($variant_slug) {
5900 + if (!is_string($variant_slug)) return [];
5901 + $config = $this->get_html_button_default_attrs();
5902 + $config['rounded'] = str_contains($variant_slug, 'pill') ? 'true' : 'false';
5903 + $config['compact'] = str_contains($variant_slug, 'mini') ? 'true' : 'false';
5904 + if (str_contains($variant_slug, 'buy-now')) {
5905 + $config['verb'] = 'buy';
5906 + } else if (str_contains($variant_slug, 'express')) {
5907 + $config['verb'] = 'express';
5908 + }
5909 + return $config;
5910 + }
5911 +
5912 + // Old legacy button logo variants. Replaced by web component. See get_html_button(). LP 2026-07-01
5913 + public function get_express_logo_variants() {
5914 + return [
5915 + 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
5916 + 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
5917 + 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
5918 + 'express-pill' => __('Express pill', 'woo-vipps'),
5919 + 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
5920 + 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
5921 + ];
5922 + }
5923 +
5924 +
5636 5925 // Whether the order is possible to restart with a retry session at VMP. LP 2026-03-18
5637 5926 public static function order_is_vipps_retryable($order_id) {
5638 5927 $order = wc_get_order($order_id);
5639 5928 if (!$order) return false;
@@ -5642,6 +5931,20 @@
5642 5931 $shipping_set = $order->get_meta('_vipps_shipping_set');
5643 5932
5644 5933 // Express or unfinalized Checkout orders do not have shipping available, so we cant retry these in particular. LP 2026-03-18
5645 5934 return $nonexpress_epayment || $shipping_set;
5935 + }
5936 +
5937 + /** Returns the plugin's rest api namespace including the version.
5938 + * Use latest version ($version = 'latest') with caution, we want backwards compatible endpoints. LP 2026-03-31 */
5939 + public static function get_rest_namespace($version = 'latest') {
5940 + $version = $version === 'latest' ? self::REST_CURRENT_VERSION : $version;
5941 + return self::REST_NAMESPACE_BASE . "/$version";
5942 + }
5943 +
5944 + /** Returns the plugin's rest api url.
5945 + * $version accepts 'latest', but you probably don't want to do that.
5946 + * Remember root forward-slash for $route. e.g $route = '/my-route' LP 2026-03-31 */
5947 + public static function get_rest_url($version, $route) {
5948 + return get_rest_url(null, static::get_rest_namespace($version) . $route, 'rest');
5646 5949 }
5647 5950 }