PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.5
Pay with Vipps and MobilePay for WooCommerce v6.2.5
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 5.4.3 5.4.2 All 187 releases
← All changes | payment/Vipps.class.php +1048 -668 5.4.2 → 6.2.5 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,43 @@
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', array($this, 'pre_special_page_actions'));
315 +
316 + // Add an admin interface for this page as well IOK 2026-09-11
317 + add_action('woocommerce_settings_pages', array($this, 'woocommerce_settings_pages'));
318 +
293 319 }
294 320
295 321 public function admin_init () {
296 -
297 322 $gw = $this->gateway();
298 323 require_once(dirname(__FILE__) . "/admin/settings/VippsAdminSettings.class.php");
299 324 $adminSettings = VippsAdminSettings::instance();
300 325 // Stuff for the Order screen
@@ -303,9 +328,9 @@
303 328 // Don't allow deletion of refunds made through Vipps IOK 2025-11-17
304 329 add_action('woocommerce_after_order_refund_item_name', function ($refund) {
305 330 $orderid = $refund->get_parent_id();
306 331 $order = wc_get_order($orderid);
307 - if (is_a($order, 'WC_Order') && $order->get_payment_method() == 'vipps') {
332 + if (is_a($order, 'WC_Order') && self::is_vipps_order($order)) {
308 333 $id = $refund->get_id();
309 334 $gw = $refund->get_refunded_payment();
310 335 if ($gw) {
311 336 $msg = sprintf(__('Refunded through %1$s', 'woo-vipps'), $this->get_payment_method_name());
@@ -327,10 +352,17 @@
327 352
328 353 // IOK 2026-05-26 redirect the old Woo-generated settings-screen to our own settings page.
329 354 add_action('current_screen', function ($screen) {
330 355 if (!is_admin() || !$screen || $screen->id !== 'woocommerce_page_wc-settings') return;
331 - if ($_GET['tab'] != 'checkout' || $_GET['section'] != 'vipps') return;
332 - wp_safe_redirect(admin_url('admin.php?page=vipps_settings_menu'));
356 + $section = ($_GET['section'] ?? "");
357 + if (($_GET['tab'] ?? "")!= 'checkout'
358 + || !in_array($section, ['vipps', 'vipps_card'])
359 + ) return;
360 +
361 + $settings_tab = '';
362 + if ('vipps_card' === $section) $settings_tab = '#Card payments';
363 +
364 + wp_safe_redirect(admin_url("admin.php?page=vipps_settings_menu$settings_tab"));
333 365 exit();
334 366 });
335 367
336 368 // Custom product properties
@@ -404,9 +436,77 @@
404 436 }
405 437 }
406 438 }
407 439
440 +
441 + /** Ensure we have a special page for payment flows
442 + *
443 + * woocommerce_loaded is too early for this because of maybe_create_vipps_pages which calls WC_Install::create_pages,
444 + * and we hook unto this with woocommerce_create_pages. LP 2026-09-03
445 + **/
446 + public function ensure_special_page_exists() {
447 + if (static::get_special_page_id()) return;
448 + $this->log(__('Missing id for special page, attempting to fix.', 'woo-vipps'), 'info');
408 449
450 + // 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
451 + $old_special_page_id = $this->gateway()->get_option('vippsspecialpageid');
452 + if ($old_special_page_id && ($special_page = get_post($old_special_page_id)) && "trash" !== $special_page->post_status) {
453 + $this->log(__('Migrated old special page setting.', 'woo-vipps'), 'info');
454 + // there is no wc_set_page_id() so we update the option directly. LP 2026-09-01
455 + update_option('woocommerce_vipps_special_page_page_id', $old_special_page_id);
456 +
457 + // Ensure this page has the necessary shortcode. LP 2026-09-01
458 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
459 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
460 + wp_update_post([
461 + 'ID' => $old_special_page_id,
462 + 'post_content' => $new_content,
463 + ]);
464 + }
465 + } else {
466 + // Create special page if its missing. LP 2026-09-01
467 + $this->maybe_create_vipps_pages();
468 + }
469 + }
470 +
471 + // Admin interface for the special page on woo/advanced/pages
472 + public function woocommerce_settings_pages ($settings) {
473 + $i = -1;
474 + foreach($settings as $entry) {
475 + $i++;
476 + if ($entry['type'] == 'sectionend' && $entry['id'] == 'advanced_page_options') {
477 + break;
478 + }
479 + }
480 + if ($i > 0) {
481 + $vippspagesettings = array(
482 + array(
483 + 'title' => sprintf(__( '%1$s Page', 'woo-vipps' ), Vipps::CompanyName()),
484 + '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') ,
485 + 'id' => 'woocommerce_vipps_special_page_page_id',
486 + 'type' => 'single_select_page_with_search',
487 + 'default' => '',
488 + 'class' => 'wc-page-search',
489 + 'css' => 'min-width:300px;',
490 + 'args' => array(
491 + 'exclude' =>
492 + array(
493 + wc_get_page_id( 'myaccount' ),
494 + wc_get_page_id( 'checkout' ),
495 + wc_get_page_id( 'cart' ),
496 + ),
497 + ),
498 + 'desc_tip' => true,
499 + 'autoload' => false,
500 + ));
501 + array_splice($settings, $i, 0, $vippspagesettings);
502 + }
503 +
504 + return $settings;
505 + }
506 +
507 +
508 +
409 509 // Runs on init, adds the Vipps badge feature if activated
410 510 public function maybe_add_vipps_badge_feature () {
411 511 $badge_options = get_option('vipps_badge_options');
412 512 if (!$badge_options || !@$badge_options['badgeon']) return false;
@@ -735,8 +835,11 @@
735 835
736 836 // Get current brand and language
737 837 $current_brand = strtolower($this->get_payment_method_name());
738 838 $current_language = $this->get_customer_language();
839 + if ('se' === $current_language) $current_language = 'sv';
840 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-13
841 + if ('dk' === $current_language) $current_language = 'da';
739 842
740 843 $variants = ['white'=> __('White', 'woo-vipps'), 'grey' => __('Grey','woo-vipps'),
741 844 'filled'=> __('Filled', 'woo-vipps'), 'light'=>__('Light','woo-vipps'),
742 845 'purple'=> __('Purple', 'woo-vipps')];
@@ -799,9 +902,9 @@
799 902
800 903 <h2><?php _e('Shortcodes', 'woo-vipps'); ?> </h2>
801 904 <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>
802 905 <br><?php _e("The shortcode looks like this:", 'woo-vipps')?><br>
803 - <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|dk} ] </pre><br>
906 + <pre>[vipps-mobilepay-badge variant={white|filled|light|grey|purple}<br> language={en|no|fi|da|sv} ] </pre><br>
804 907 <?php _e("Please refer to the documentation for the meaning of the parameters.", 'woo-vipps'); ?></br>
805 908 <?php _e("The brand will be automatically applied.", 'woo-vipps'); ?>
806 909 </p>
807 910
@@ -828,21 +931,24 @@
828 931 echo json_encode(array('ok'=>0,'msg'=>__('You don\'t have sufficient rights to edit this product', 'woo-vipps')));
829 932 wp_die(__('You don\'t have sufficient rights to edit this product', 'woo-vipps'));
830 933 }
831 934
832 - $options = get_option('vipps_button_options');
833 - if (isset($_POST['express']['variant'])) {
834 - $options['express']['variant'] = sanitize_title($_POST['express']['variant']);
935 + $old = get_option('vipps_button_options2', []);
936 + $new = $old;
937 + if (isset($_POST['express']['configs'])) {
938 + foreach ($_POST['express']['configs'] as $ctx => $config) {
939 + $sanitized_ctx = sanitize_title($ctx);
940 + $sanitized_config = map_deep($config, 'sanitize_title');
941 +
942 + // If nonglobal context that uses global config, just wipe the rest of the stored config. LP 2026-06-25
943 + if ("global" !== $sanitized_ctx && ($sanitized_config['use-global-config'] ?? false)) {
944 + $new['express']['configs'][$sanitized_ctx] = ['use-global-config' => true];
945 + } else {
946 + $new['express']['configs'][$sanitized_ctx] = $sanitized_config;
947 + }
948 + }
835 949 }
836 - if (isset($_POST['express']['mini-variant'])) {
837 - $options['express']['mini-variant'] = sanitize_title($_POST['express']['mini-variant']);
838 - }
839 - if (isset($_POST['express']['force-mini']) && is_array($_POST['express']['force-mini'])) {
840 - foreach($_POST['express']['force-mini'] as $key => $val)
841 - $options['express']['force-mini'][$key] = sanitize_title($val);
842 - }
843 -
844 - update_option('vipps_button_options', $options);
950 + update_option('vipps_button_options2', $new);
845 951 wp_safe_redirect(admin_url("admin.php?page=vipps_button_menu"));
846 952 exit();
847 953 }
848 954
@@ -876,9 +982,12 @@
876 982 public function vipps_mobilepay_badge_shortcode($atts) {
877 983 $args = shortcode_atts( array('id'=>'', 'class'=>'', 'brand' => '', 'variant' => '','language'=>''), $atts );
878 984
879 985 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple', 'filled', 'light']) ? $args['variant'] : "";
880 - $language = in_array($args['language'], ['en','no', 'fi', 'dk']) ? $args['language'] : $this->get_customer_language();
986 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
987 + if ('se' === $language) $language = 'sv';
988 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
989 + if ('dk' === $language) $language = 'da';
881 990 $id = sanitize_title($args['id']);
882 991 $class = sanitize_text_field($args['class']);
883 992
884 993 $attributes = [];
@@ -894,13 +1003,18 @@
894 1003 return "<vipps-mobilepay-badge $badgeatts></vipps-mobilepay-badge>";
895 1004 }
896 1005
897 1006 // legacy vipps_badge shortcode, the new one is vipps_mobilepay_badge_shortcode. LP 19.11.2024
1007 + // Diff: this one doesn't support brand (JUST VIPPS). LP 2026-08-11
898 1008 public function vipps_badge_shortcode($atts) {
899 1009 $args = shortcode_atts( array('id'=>'', 'class'=>'','variant' => '','language'=>''), $atts );
900 1010
901 1011 $variant = in_array($args['variant'], ['orange', 'light-orange', 'grey','white', 'purple']) ? $args['variant'] : "";
902 - $language = in_array($args['language'], ['en','no', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1012 + $language = in_array($args['language'], ['en', 'no', 'sv', 'da', 'dk', 'fi']) ? $args['language'] : $this->get_customer_language();
1013 + if ('se' === $language) $language = 'sv';
1014 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1015 + if ('dk' === $language) $language = 'da';
1016 +
903 1017 $id = sanitize_title($args['id']);
904 1018 $class = sanitize_text_field($args['class']);
905 1019
906 1020 $attributes = [];
@@ -914,163 +1028,339 @@
914 1028
915 1029 return "<vipps-badge $badgeatts></vipps-badge>";
916 1030 }
917 1031
918 - public function get_express_logo_variants() {
1032 + public function get_html_button_default_attrs() {
919 1033 return [
920 - 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
921 - 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
922 - 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
923 - 'express-pill' => __('Express pill', 'woo-vipps'),
924 - 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
925 - 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
1034 + 'language' => 'store',
1035 + 'variant' => 'primary',
1036 + 'rounded' => 'false',
1037 + 'verb' => 'buy',
1038 + 'stretched' => 'false',
1039 + 'compact' => 'false',
1040 + '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
926 1041 ];
927 1042 }
928 1043
1044 + public function get_html_button_attrs_for_context($context = 'global') {
1045 + $options = get_option('vipps_button_options2', []);
1046 + if (!is_string($context)) $context = 'global';
1047 +
1048 + // Gutenberg express checkout buttons really want to be stretched, so we'll treat them somewhat differently.
1049 + $gutenberg = false;
1050 + if ($context == 'checkout_gutenberg') {
1051 + $context = 'checkout';
1052 + $gutenberg = true;
1053 + }
1054 + if ($context == 'cart_gutenberg') {
1055 + $context = 'cart';
1056 + $gutenberg = true;
1057 + }
929 1058
930 - public function button_menu_page() {
931 - if (!current_user_can('manage_woocommerce')) {
932 - wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
1059 + $config = $options['express']['configs'][$context] ?? [];
1060 + $use_global = !$config || ($config['use-global-config'] ?? false);
1061 + if ($use_global) {
1062 + $config = $options['express']['configs']['global'] ?? $this->get_html_button_default_attrs();
933 1063 }
1064 +
1065 + // see above.
1066 + if ($gutenberg) {
1067 + $config['stretched']='true';
1068 + }
1069 + return $config;
1070 + }
1071 +
1072 + public function get_html_button_for_context($context = 'global') {
1073 + return $this->get_html_button($this->get_html_button_attrs_for_context($context));
1074 + }
1075 +
1076 + // Generic Vipps/MobilePay button html, as of now a web component hosted locally. LP 2026-06-24
1077 + // See info and attributes at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1078 + public function get_html_button($attrs = []) {
934 1079 $payment_method = $this->get_payment_method_name();
935 - $lang = $this->get_customer_language();
936 - $button_options = get_option('vipps_button_options');
1080 + $attrs = wp_parse_args($attrs, $this->get_html_button_default_attrs());
1081 + $attrs['brand'] = strtolower($payment_method);
1082 + $attrs['type'] = 'button'; // static
937 1083
938 - $variants = $this->get_express_logo_variants();
939 - $mini_variants = array_filter($variants, fn($key) => str_ends_with($key, 'mini'), ARRAY_FILTER_USE_KEY);
1084 + // Support using store language
1085 + if ('store' === $attrs['language']) $attrs['language'] = $this->get_customer_language();
1086 + // Don't support these login verbs. LP 2026-06-04
1087 + if (in_array($attrs['verb'], ['login', 'register'])) $attrs['verb'] = 'buy';
1088 + // Looks like button and badge web components now use 'da' instead of 'dk' for danish. LP 2026-08-11
1089 + if ('dk' === $attrs['language']) $attrs['language'] = 'da';
940 1090
941 - $init_states = [
942 - 'express' => [
943 - 'variant' => array_key_exists(@$button_options['express']['variant'], $variants) ? $button_options['express']['variant'] : 'buy-now-rectangular',
944 - 'mini-variant' => array_key_exists(@$button_options['express']['mini-variant'], $mini_variants) ? $button_options['express']['mini-variant'] : 'express-rectangular-mini',
945 - 'force-mini' => [
946 - 'product' => @$button_options['express']['force-mini']['product'] ?? 'no',
947 - 'catalog' => @$button_options['express']['force-mini']['catalog'] ?? 'yes',
948 - 'cart' => @$button_options['express']['force-mini']['cart'] ?? 'no',
949 - 'minicart' => @$button_options['express']['force-mini']['minicart'] ?? 'no',
950 - ],
951 - ],
952 - ];
1091 + $escaped_attrs = [];
1092 + foreach($attrs as $k => $v) {
1093 + $escaped_attrs[$k] = esc_attr($v);
1094 + }
953 1095
1096 + // id attribute
1097 + $id = $escaped_attrs['id'] ?? '';
1098 + $id_str = $id ? "id='$id'" : '';
1099 +
1100 + // class attribute
1101 + $class_str = '';
1102 + if (isset($attrs['class'])) {
1103 + if (is_array($attrs['class'])) {
1104 + $class_str = implode(' ', $attrs['class']);
1105 + } else if (is_string($attrs['class'])) {
1106 + $class_str = $attrs['class'];
1107 + }
1108 + }
1109 +
1110 + // The html
1111 + $html = <<<EOF
1112 +<vipps-mobilepay-button
1113 + $id_str
1114 + $class_str
1115 + type="{$escaped_attrs['type']}"
1116 + brand="{$escaped_attrs['brand']}"
1117 + language="{$escaped_attrs['language']}"
1118 + variant="{$escaped_attrs['variant']}"
1119 + rounded="{$escaped_attrs['rounded']}"
1120 + verb="{$escaped_attrs['verb']}"
1121 + stretched="{$escaped_attrs['stretched']}"
1122 + compact="{$escaped_attrs['compact']}"
1123 +></vipps-mobilepay-button>
1124 +EOF;
1125 + return apply_filters('woo_vipps_html_button', $html, $attrs);
1126 + }
1127 +
1128 + public function button_menu_page() {
1129 + if (!current_user_can('manage_woocommerce')) {
1130 + wp_die(__('You don\'t have sufficient rights to access this page', 'woo-vipps'));
1131 + }
1132 + wp_enqueue_script('vipps-button-webcomponent');
954 1133 ?>
955 1134 <div class='wrap vipps-button-settings'>
956 - <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
957 - <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
958 - <form class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1135 + <h1><?php echo sprintf(__('%1$s button configuration', 'woo-vipps'), Vipps::CompanyName()); ?></h1>
1136 + <span><?php echo sprintf(__('%1$s supports different variants of buttons for you to perfect your store\'s look', 'woo-vipps'), Vipps::CompanyName()); ?></span>
1137 + <form id="vipps-button-settings-form" class="vipps-button-settings" action="<?php echo admin_url('admin-post.php'); ?>" method="POST">
1138 + <input type="hidden" name="action" value="update_vipps_button_settings" />
1139 + <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
959 1140
960 - <!-- EXPRESS SECTION -->
961 - <div id="vipps-button-settings-express-container">
962 - <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
963 - <input type="hidden" name="action" value="update_vipps_button_settings" />
964 - <?php wp_nonce_field( 'buttonaction', 'buttonnonce'); ?>
1141 + <!-- Express section -->
1142 + <?php $this->button_menu_express_section(); ?>
965 1143
966 - <!-- variant -->
967 - <div class="vipps-button-settings-section">
968 - <!-- variant dropdown -->
969 - <div class="vipps-button-settings-express-demo-container">
970 - <label for="vippsButtonVariant"><?php _e('Choose variant', 'woo-vipps'); ?></label>
971 - <select id="vippsButtonVariant" name="express[variant]" onChange='changeExpressVariant()'>
972 - <?php foreach($variants as $key=>$name): ?>
973 - <option value="<?php echo $key; ?>" <?php if ($init_states['express']['variant'] === $key) echo " selected "; ?> >
974 - <?php echo $name ; ?>
975 - </option>
976 - <?php endforeach; ?>
977 - </select>
1144 + <!-- submit button -->
1145 + <div id="vipps-button-settings-save">
1146 + <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
978 1147 </div>
1148 + </form>
1149 + </div>
1150 + <?php
1151 + }
979 1152
980 - <!-- Preload all variant images. Javascript will show the active one. LP 2025-12-16 -->
981 - <div class="vipps-button-settings-express-demo-container vipps-button-settings-img-container">
982 - <?php foreach(array_keys($variants) as $variant): ?>
983 - <img
984 - class="vipps-button-settings-express-demo"
985 - id="vipps-button-settings-express-demo-<?php echo $variant; ?>"
986 - src="<?php echo $this->get_express_logo($payment_method, $lang, $variant); ?>"
987 - style="display: <?php echo ($variant === $init_states['express']['variant'] ? 'block' : 'none') ;?>;"
988 - >
989 - <?php endforeach; ?>
990 - </div>
991 - </div>
1153 + private function button_menu_express_section() {
1154 + $options = get_option('vipps_button_options2', []);
1155 + $express = $options['express'] ?? [];
1156 + $configs = $express['configs'] ?? [];
992 1157
993 1158
994 - <!-- mini variant section -->
995 - <div class="vipps-button-settings-section">
996 - <!-- Checkboxes "Use mini version for x page" -->
997 - <label><?php _e('Force mini variant in these contexts:', 'woo-vipps'); ?></label>
998 - <div class="vipps-button-settings-express-force-mini-container">
999 - <label class="vipps-button-settings-express-force-mini" id="vipps-button-settings-express-force-mini-product"><?php _e('Product page', 'woo-vipps'); ?></label>
1000 - <input name="express[force-mini][product]" type="hidden" value="no">
1001 - <input name="express[force-mini][product]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['product'] == "yes") echo "checked";?>>
1002 - </div>
1159 + $contexts = [
1160 + 'global' => __('Global', 'woo-vipps'),
1161 + 'product' => __('Product', 'woo-vipps'),
1162 + 'catalog' => __('Catalog', 'woo-vipps'),
1163 + 'cart' => __('Cart', 'woo-vipps'),
1164 + 'minicart' => __('Mini cart', 'woo-vipps'),
1165 + 'checkout' => __('Checkout', 'woo-vipps'),
1166 + ];
1167 + $init_context = 'global';
1168 + $init_config = $configs[$init_context] ?? [];
1003 1169
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-catalog"><?php _e('Catalog page', 'woo-vipps'); ?></label>
1006 - <input name="express[force-mini][catalog]" type="hidden" value="no">
1007 - <input name="express[force-mini][catalog]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['catalog'] == "yes") echo "checked";?>>
1008 - </div>
1170 + // html button args
1171 + $init_args = $init_config;
1172 + $init_args['id'] = 'vipps-button-express-preview';
1009 1173
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-cart"><?php _e('Cart', 'woo-vipps'); ?></label>
1012 - <input name="express[force-mini][cart]" type="hidden" value="no">
1013 - <input name="express[force-mini][cart]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['cart'] == "yes") echo "checked";?>>
1014 - </div>
1174 + ?>
1175 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-container">
1176 + <h2> <?php _e('Express Checkout', 'woo-vipps'); ?></h2>
1015 1177
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-minicart"><?php _e('Mini cart', 'woo-vipps'); ?></label>
1018 - <input name="express[force-mini][minicart]" type="hidden" value="no">
1019 - <input name="express[force-mini][minicart]" type="checkbox" value="yes" <?php if ($init_states['express']['force-mini']['minicart'] == "yes") echo "checked";?>>
1020 - </div>
1178 + <!-- Context dropdown -->
1179 + <div id="vipps-button-settings-express-context">
1180 + <label>
1181 + <?php _e('Config context', 'woo-vipps'); ?>
1182 + </label>
1183 + <select id="context" onChange='updateContext()'>
1184 + <?php foreach($contexts as $key => $label): ?>
1185 + <option value="<?php echo $key; ?>" <?php if ('global' === $key) echo " selected "; ?> >
1186 + <?php echo $label ; ?>
1187 + </option>
1188 + <?php endforeach; ?>
1189 + </select>
1190 + <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>
1191 + </div>
1192 +
1021 1193
1022 - <!-- mini variant dropdown -->
1023 - <div class="vipps-button-settings-express-mini-demo-container">
1024 - <label for="vippsButtonMiniVariant"><?php _e('Choose variant to use in mini contexts', 'woo-vipps'); ?></label>
1025 - <select id="vippsButtonMiniVariant" name="express[mini-variant]" onChange='changeExpressMiniVariant()'>
1026 - <?php foreach($mini_variants as $key=>$name): ?>
1027 - <option value="<?php echo $key; ?>" <?php if ($init_states['express']['mini-variant'] === $key) echo " selected "; ?> >
1028 - <?php echo $name ; ?>
1029 - </option>
1030 - <?php endforeach; ?>
1031 - </select>
1032 - </div>
1194 + <!-- Button paremeter inputs. These input values are put into post data express.tmpConfig temporarily.
1195 + On context change, configs are stored in a global 'contextConfigs'. Each config is processed into new option structure before submit. LP 2026-06-24 -->
1196 + <div class="vipps-button-settings-section" id="vipps-button-settings-express-args">
1197 + <fieldset>
1198 + <label><input type="checkbox" name="express[tmpConfig][rounded]" checked=""><?php _e('Rounded', 'woo-vipps'); ?></label>
1199 + <label><input type="checkbox" name="express[tmpConfig][compact]"><?php _e('Compact', 'woo-vipps'); ?></label>
1200 + <label><input type="checkbox" name="express[tmpConfig][stretched]"><?php _e('Stretched', 'woo-vipps'); ?></label>
1201 + </fieldset>
1202 + <fieldset>
1203 + <legend><?php _e('Language', 'woo-vipps'); ?></legend>
1204 + <label><input type="radio" name="express[tmpConfig][language]" checked value="store"><?php _e('Store language', 'woo-vipps'); ?></label>
1205 + <label><input type="radio" name="express[tmpConfig][language]" value="en"><?php _e('English', 'woo-vipps'); ?></label>
1206 + <label><input type="radio" name="express[tmpConfig][language]" value="no"><?php _e('Norwegian', 'woo-vipps'); ?></label>
1207 + <label><input type="radio" name="express[tmpConfig][language]" value="dk"><?php _e('Danish', 'woo-vipps'); ?></label>
1208 + <label><input type="radio" name="express[tmpConfig][language]" value="sv"><?php _e('Swedish', 'woo-vipps'); ?></label>
1209 + <?php if ($this->get_payment_method_name() === 'MobilePay'): ?>
1210 + <label><input type="radio" disabled="" name="express[tmpConfig][language]" value="fi"><?php _e('Finnish', 'woo-vipps'); ?></label>
1211 + <?php endif; ?>
1212 + </fieldset>
1033 1213
1034 - <!-- Preload mini variant imgs. LP 2025-12-17 -->
1035 - <div class="vipps-button-settings-express-mini-demo-container vipps-button-settings-img-container">
1036 - <?php foreach(array_keys($mini_variants) as $variant): ?>
1037 - <img
1038 - class="vipps-button-settings-express-mini-demo"
1039 - id="vipps-button-settings-express-mini-demo-<?php echo $variant; ?>"
1040 - src="<?php echo $this->get_express_logo($payment_method, $lang, $variant); ?>"
1041 - style="display: <?php echo ($variant === $init_states['express']['mini-variant'] ? 'block' : 'none') ;?>;"
1042 - >
1043 - <?php endforeach; ?>
1044 - </div>
1045 - </div>
1214 + <?php if ($this->get_payment_method_name() !== 'MobilePay'): ?>
1215 + <p><?php printf(__('Finnish is currently only available with the %s payment method.', 'woo-vipps'), 'MobilePay'); ?></p>
1216 + <?php endif; ?>
1046 1217
1047 - <!-- END EXPRESS SECTION -->
1218 + <fieldset>
1219 + <legend><?php _e('Verb', 'woo-vipps'); ?></legend>
1220 + <label><input type="radio" name="express[tmpConfig][verb]" checked value="buy"><?php _e('Buy', 'woo-vipps'); ?></label>
1221 + <label><input type="radio" name="express[tmpConfig][verb]" value="pay"><?php _e('Pay', 'woo-vipps'); ?></label>
1222 + <label><input type="radio" name="express[tmpConfig][verb]" value="continue"><?php _e('Continue', 'woo-vipps'); ?></label>
1223 + <label><input type="radio" name="express[tmpConfig][verb]" value="confirm"><?php _e('Confirm', 'woo-vipps'); ?></label>
1224 + <label><input type="radio" name="express[tmpConfig][verb]" value="donate"><?php _e('Donate', 'woo-vipps'); ?></label>
1225 + <label><input type="radio" name="express[tmpConfig][verb]" value="express"><?php _e('Express', 'woo-vipps'); ?></label>
1226 + </fieldset>
1227 + <fieldset>
1228 + <legend><?php _e('Variant', 'woo-vipps'); ?></legend>
1229 + <label><input type="radio" name="express[tmpConfig][variant]" checked value="primary"><?php _e('Primary', 'woo-vipps'); ?></label>
1230 + <label><input type="radio" name="express[tmpConfig][variant]" value="dark"><?php _e('Dark (WCAG AAA)', 'woo-vipps'); ?></label>
1231 + <label><input type="radio" name="express[tmpConfig][variant]" value="light"><?php _e('Light (WCAG AAA)', 'woo-vipps'); ?></label>
1232 + </fieldset>
1048 1233 </div>
1049 1234
1050 - <!-- Save button -->
1051 - <div id="vipps-button-settings-save">
1052 - <input class="btn button primary" type="submit" value="<?php _e('Update settings', 'woo-vipps'); ?>" />
1053 - </div>
1054 -
1055 - </form>
1235 + <!-- Button preview that changes depending on the chosen parameters. LP 2026-06-24 -->
1236 + <?php echo $this->get_html_button($init_args); ?>
1056 1237 </div>
1057 1238
1058 1239 <script>
1059 - function changeExpressVariant() {
1060 - const variant = jQuery('#vippsButtonVariant').val().trim();
1061 - // Show the one selected, hide all others. LP 2025-12-16
1062 - jQuery('.vipps-button-settings-express-demo').hide();
1063 - jQuery(`#vipps-button-settings-express-demo-${variant}`).show();
1064 - }
1240 + // When inputs change, update the preview args. LP 2026-06-24
1241 + jQuery('#vipps-button-settings-express-args input').on('click', updatePreview);
1065 1242
1066 - function changeExpressMiniVariant() {
1067 - const variant = jQuery('#vippsButtonMiniVariant').val().trim();
1068 - // Show the one selected, hide all others. LP 2025-12-16
1069 - jQuery('.vipps-button-settings-express-mini-demo').hide();
1070 - jQuery(`#vipps-button-settings-express-mini-demo-${variant}`).show();
1071 - }
1072 - </script>
1243 + let currentContext = '<?php echo $init_context; ?>';
1244 + let contextConfigs = <?php echo json_encode($configs) ?: "{}"; ?> // maps context slug to config object. LP 2026-06-24
1245 +
1246 + // Updates the actual html inputs from given config. LP 2026-07-01
1247 + function setInputsFromConfig(context, config) {
1248 + const useGlobalConfig = Boolean(config?.["use-global-config"]);
1249 + const isGlobal = "global" === context;
1250 +
1251 + // Only show the 'use-global-config' checkbox for nonglobal context. LP 2026-06-26
1252 + jQuery('#use-global-config-container').toggleClass('hidden', isGlobal);
1253 +
1254 + // Nonglobal contexts with useGlobalConfig, and empty configs, should fallback to the global config. LP 2026-06-26
1255 + if (!config || (!isGlobal && useGlobalConfig)) {
1256 + config = contextConfigs["global"];
1257 +
1258 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", true);
1259 +
1260 + // When using global config, the inputs should be disabled until its unchecked. LP 2026-06-26
1261 + jQuery('#vipps-button-settings-express-args input').prop("disabled", true);
1262 + } else {
1263 + jQuery('input[name="express[tmpConfig][use-global-config]"]').prop("checked", false);
1264 + jQuery('#vipps-button-settings-express-args input').prop("disabled", false);
1265 + }
1266 +
1267 + Object.entries(config).forEach(([key, val]) => {
1268 + if ("use-global-config" === key) return;
1269 + const inputs = jQuery(`input[name="express[tmpConfig][${key}]"]`);
1270 + const type = inputs.prop('type');
1271 + switch (type) {
1272 + case "checkbox":
1273 + inputs.prop('checked', typeof val === "boolean" ? val : "true" === val);
1274 + break;
1275 + case "radio":
1276 + inputs.filter(`[value="${val}"]`).prop('checked', true);
1277 + break;
1278 + default:
1279 + console.error(`woo-vipps: Unexpected input type '${type}' for button config. key=${key}, val=${val}`);
1280 + }
1281 + });
1282 +
1283 + updatePreview();
1284 + }
1285 + // init the starting config from option. LP 2026-06-25
1286 + setInputsFromConfig(currentContext, contextConfigs[currentContext]);
1287 +
1288 + // Update the preview web component's attributes. LP 2026-06-24
1289 + function updatePreview(event) {
1290 + const args = getPreviewArgs();
1291 + // 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
1292 + // if ('store' === args.language) args.language = '<?php echo $this->get_customer_language(); ?>';
1293 + if ('store' === args.language) args.language = '<?php echo substr(get_locale(), 0, 2); ?>';
1294 + const button = jQuery('#vipps-button-express-preview');
1295 + button.attr(args);
1296 + }
1297 +
1298 + function getPreviewArgs() {
1299 + const args = {};
1300 + jQuery('#vipps-button-settings-express-args input').each(function () {
1301 + // inputs are put in form arrays like 'express[tmpConfig][attribute]', so extract the actual attribute name. LP 2026-06-24
1302 + const matches = [...this.name.matchAll(/\[([^\]]+)\]/g)];
1303 + const attr = matches.length ? matches[matches.length - 1][1] : null;
1304 + if (!attr) {
1305 + console.error("woo-vipps: Could not extract attribute name for button preview:", this);
1306 + return;
1307 + }
1308 + if (this.type === 'checkbox') {
1309 + args[attr] = this.checked;
1310 + } else if (this.checked) {
1311 + args[attr] = this.value;
1312 + }
1313 + });
1314 +
1315 + return args;
1316 + }
1317 +
1318 + // Stores selected config for context and switches to another (if changed). LP 2026-07-01
1319 + function updateContext() {
1320 + const wasGlobal = "global" === currentContext;
1321 + const useGlobalConfig = jQuery('#use-global-config-container input').prop("checked");
1322 +
1323 + // Store config to global, unless its a non-global context that uses global config. LP 2026-06-25
1324 + if (wasGlobal || !useGlobalConfig) {
1325 + contextConfigs[currentContext] = getPreviewArgs();
1326 + } else {
1327 + contextConfigs[currentContext] = {'use-global-config': true};
1328 + }
1329 +
1330 + // Swap to new context: set all input fields to the stored values if exists. LP 2026-06-25
1331 + const newContext = jQuery("#context").val();
1332 + const newConfig = contextConfigs[newContext];
1333 +
1334 + setInputsFromConfig(newContext, newConfig);
1335 + currentContext = newContext;
1336 + }
1337 +
1338 + // Before submit: delete the tmpConfig for the current selected values, and add the stored contextConfigs to the post data. LP 2026-06-24
1339 + jQuery('#vipps-button-settings-form').on('formdata', e => {
1340 + const formData = e?.originalEvent?.formData;
1341 + if (!formData) return;
1342 +
1343 + // run this to store current context config before posting. LP 2026-06-24
1344 + updateContext();
1345 +
1346 + // now we can delete the current temporary config from post data. LP 2026-06-24
1347 + const keysToDelete = [];
1348 + for (const [key] of formData.entries()) {
1349 + if (key.startsWith("express[tmpConfig][")) {
1350 + keysToDelete.push(key);
1351 + }
1352 + }
1353 + keysToDelete.forEach(key => formData.delete(key));
1354 +
1355 + // Now add the actual post data from the stored global contextConfigs. LP 2026-06-24
1356 + Object.entries(contextConfigs).forEach(([context, config]) => {
1357 + Object.entries(config).forEach(([key, val]) => {
1358 + formData.append(`express[configs][${context}][${key}]`, val);
1359 + });
1360 + });
1361 + });
1362 + </script>
1073 1363 <?php
1074 1364 }
1075 1365
1076 1366
@@ -1323,22 +1613,35 @@
1323 1613 public function delete_old_cancelled_orders() {
1324 1614 $limit = 30;
1325 1615 $cutoff = time() - 600; // Ten minutes old orders: Delete them
1326 1616 $oldorders = time() - (60*60*24*7); // Very old orders: Ignore them to make this work on sites with enormous order databases
1327 - // Ensure the old order table understands the meta query IOK 2022-12-02
1328 - static::add_wc_order_meta_key_support();
1329 - $args = array(
1617 + $delenda = [];
1618 +
1619 + if ($this->useHPOS()) {
1620 + $args = array(
1330 1621 'status' => 'cancelled',
1331 1622 'limit' => $limit,
1332 1623 'date_modified' => "$oldorders...$cutoff",
1333 - 'meta_vipps_delendum' => 1);
1334 - if ($this->useHPOS()) {
1335 - /* The above, with the filter, is for the old orders table, the below is for the new IOK 2022-12-02 */
1336 - $args['meta_query'] = [[ 'key' => '_vipps_delendum', 'value' => 1 ]];
1624 + 'meta_query' => [[ 'key' => '_vipps_delendum', 'value' => 1 ]]
1625 + );
1626 + $delenda = wc_get_orders($args);
1627 + } else {
1628 + // Old-style orders, we'll just use SQL
1629 + global $wpdb;
1630 + $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",
1631 + gmdate( 'Y-m-d H:i:s', $oldorders ),
1632 + gmdate( 'Y-m-d H:i:s', $cutoff ),
1633 + $limit
1634 + );
1635 + $order_ids = $wpdb->get_col($sql);
1636 + foreach($order_ids as $did) {
1637 + $d = wc_get_order($did);
1638 + if ($d && !is_wp_error($d)) {
1639 + $delenda[] = $d;
1640 + }
1641 + }
1337 1642 }
1338 1643
1339 - $delenda = wc_get_orders($args);
1340 -
1341 1644 foreach ($delenda as $del) {
1342 1645 // Delete only if there is no customer info for the order IOK 2022-10-12
1343 1646 if (!$del->get_billing_email()) {
1344 1647 $del->delete(true);
@@ -1454,9 +1757,9 @@
1454 1757 $orderid = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0;
1455 1758 $order = wc_get_order($orderid);
1456 1759 }
1457 1760 }
1458 - if (is_a($order, 'WC_Order') && $order->get_payment_method() == 'vipps') {
1761 + if (is_a($order, 'WC_Order') && self::is_vipps_order($order)) {
1459 1762 $vippsorder = true;
1460 1763 }
1461 1764
1462 1765 if ($vippsorder) {
@@ -1472,20 +1775,37 @@
1472 1775 }
1473 1776 wp_register_script('vipps-gw',plugins_url('js/vipps.js',__FILE__),array('jquery','wp-hooks'),filemtime(dirname(__FILE__) . "/js/vipps.js"), 'true');
1474 1777
1475 1778 // Badges - web components provided by Vipps MobilePay to display payment options in-store.
1476 - wp_register_script('vipps-onsite-messageing','https://checkout.vipps.no/on-site-messaging/v1/vipps-osm.js',array(),WOO_VIPPS_VERSION,
1477 - array(
1779 + wp_register_script('vipps-onsite-messageing',
1780 + plugins_url('js/vipps-on-site-messaging.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1781 + array(),
1782 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-on-site-messaging.js'),
1783 + [
1478 1784 'in_footer' => true,
1479 1785 'strategy' => 'async',
1480 - ));
1786 + ],
1787 + );
1481 1788
1789 + // Button web component downloaded from https://cdn.vippsmobilepay.com/js/button/button.js. LP 2026-06-24
1790 + wp_register_script('vipps-button-webcomponent',
1791 + plugins_url('js/vipps-button.js', WC_VIPPS_PAYMENT_MAIN_FILE),
1792 + array(),
1793 + filemtime(dirname(WC_VIPPS_PAYMENT_MAIN_FILE) . '/js/vipps-button.js'),
1794 + [
1795 + 'in_footer' => false
1796 + ],
1797 + );
1482 1798 }
1483 1799
1484 1800 // Runs late in both wp_enqueue_scripts and admin_enqueue_scripts to make it more compatible with translation plugins IOK 2026-02-02
1485 1801 public function script_add_vippslocale () {
1486 1802 // This is actually for the payment block, where localize script has started to not-work in certain contexts. IOK 2022-12-13
1487 - $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()));
1803 + $strings = array(
1804 + 'Continue with Vipps'=>sprintf(__('Continue with %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1805 + 'Vipps'=> sprintf(__('%1$s', 'woo-vipps'), $this->get_payment_method_name()),
1806 + 'pay_with_card' => sprintf(__('Pay with card through %1$s', 'woo-vipps'), $this->get_payment_method_name()),
1807 + );
1488 1808 wp_localize_script('vipps-gw', 'VippsLocale', $strings);
1489 1809 }
1490 1810
1491 1811 public function wp_enqueue_scripts() {
@@ -1494,8 +1814,9 @@
1494 1814 $this->script_add_vippslocale();
1495 1815
1496 1816 wp_enqueue_script('vipps-gw');
1497 1817 wp_enqueue_style('vipps-gw',plugins_url('css/vipps.css',__FILE__),array(),filemtime(dirname(__FILE__) . "/css/vipps.css"));
1818 + wp_enqueue_script('vipps-button-webcomponent');
1498 1819 }
1499 1820
1500 1821
1501 1822 public function add_shortcodes() {
@@ -1507,8 +1828,11 @@
1507 1828 // New vipps-mobilepay-badge shortcode. LP 19.11.2024
1508 1829 add_shortcode('vipps-mobilepay-badge', array($this, 'vipps_mobilepay_badge_shortcode'));
1509 1830 // Legacy vipps-badge shortcode. LP 19.11.2024
1510 1831 add_shortcode('vipps-badge', array($this, 'vipps_badge_shortcode'));
1832 +
1833 + // special page handling, previously a fake page. LP 2026-08-25
1834 + add_shortcode('vipps_special_page', array($this, 'vipps_special_page_shortcode'));
1511 1835 }
1512 1836
1513 1837
1514 1838 public function log ($what,$type='info') {
@@ -1534,8 +1858,10 @@
1534 1858 }
1535 1859
1536 1860 // Show express button option on checkout form. LP 2026-03-23
1537 1861 public function checkout_before_customer_details_express () {
1862 + if (did_action('woo_vipps_checkout_before_customer_details_express')) return;
1863 + do_action('woo_vipps_checkout_before_customer_details_express');
1538 1864 $gw = $this->gateway();
1539 1865 if (!$gw->show_express_checkout()) return;
1540 1866 $this->express_checkout_section_html();
1541 1867 }
@@ -1542,13 +1868,13 @@
1542 1868
1543 1869 public function express_checkout_section_html() {
1544 1870 $payment_method = $this->get_payment_method_name();
1545 1871 $header_text = __('Express Checkout', 'woo-vipps');
1546 - $header = "<div class='express-header'>$header_text</div>";
1872 + $header = "<legend class='express-header'>$header_text</legend>";
1547 1873 $div_classes = "legacy-checkout vipps-express-checkout $payment_method";
1548 - echo "<div class='$div_classes'>$header";
1549 - $this->cart_express_checkout_button_html();
1550 - echo '</div>';
1874 + echo "<fieldset class='$div_classes'>$header";
1875 + $this->checkout_express_checkout_button_html();
1876 + echo '</fieldset>';
1551 1877 }
1552 1878
1553 1879 public function express_checkout_banner() {
1554 1880 $gw = $this->gateway();
@@ -1574,8 +1900,27 @@
1574 1900 <div class="<?php echo $div_classes;?>"><?php echo $message;?></div>
1575 1901 <?php
1576 1902 }
1577 1903
1904 + public function checkout_express_checkout_button() {
1905 + $gw = $this->gateway();
1906 +
1907 + if ($gw->show_express_checkout()){
1908 + return $this->checkout_express_checkout_button_html();
1909 + }
1910 + }
1911 +
1912 + public function checkout_express_checkout_button_html() {
1913 + $url = $this->express_checkout_url();
1914 + $url = wp_nonce_url($url,'express','sec');
1915 + $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context('checkout'));
1916 + $method = $this->get_payment_method_name();
1917 + $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1918 + $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
1919 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1920 + echo $html;
1921 + }
1922 +
1578 1923 // Show the express button if reasonable to do so
1579 1924 public function cart_express_checkout_button() {
1580 1925 $gw = $this->gateway();
1581 1926
@@ -1587,29 +1932,52 @@
1587 1932 public function minicart_express_checkout_button() {
1588 1933 $gw = $this->gateway();
1589 1934
1590 1935 if ($gw->show_express_checkout()){
1591 - return $this->cart_express_checkout_button_html(true);
1936 + return $this->cart_express_checkout_button_html('minicart');
1592 1937 }
1593 1938 }
1594 1939
1595 - public function cart_express_checkout_button_html($minicart = false) {
1940 + public function cart_express_checkout_button_html($context= 'cart') {
1596 1941 $url = $this->express_checkout_url();
1597 1942 $url = wp_nonce_url($url,'express','sec');
1598 - $page = $minicart ? 'minicart' : 'cart';
1599 - $imgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo($page));
1943 + $button= apply_filters('woo_vipps_express_checkout_button', $this->get_html_button_for_context($context));
1600 1944 $method = $this->get_payment_method_name();
1601 1945 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $method);
1602 - $button = "<a href='$url' class='button vipps-express-checkout short $method' title='$title'><img alt='$title' border=0 src='$imgurl'></a>";
1603 - $button = apply_filters('woo_vipps_cart_express_checkout_button', $button, $url);
1604 - echo $button;
1946 + $html = "<a href='$url' class='vipps-express-checkout short $method' title='$title'>$button</a>";
1947 + $html = apply_filters('woo_vipps_cart_express_checkout_button', $html, $url);
1948 + echo $html;
1605 1949 }
1606 1950
1607 1951 // 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
1608 1952 // cached. Therefore stock, purchasability etc will be done later. IOK 2018-10-02
1609 1953 public function buy_now_button_shortcode ($atts) {
1610 - $args = shortcode_atts( array( 'id' => '','variant'=>'','sku' => '',), $atts );
1611 - return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode') . "</div>";
1954 + // The new web component button args. LP 2026-07-02
1955 + $button_args = $this->get_html_button_default_attrs();
1956 + unset($button_args['brand']);
1957 +
1958 + // Variant exists for the product variant. LP 2026-07-02
1959 + if (isset($button_args['variant'])) $button_args['button_variant'] = $button_args['variant'];
1960 + unset($button_args['variant']);
1961 +
1962 + $args = shortcode_atts(
1963 + array(...$button_args,
1964 + 'id' => '','variant'=> '','sku' => '',
1965 + ),
1966 + $atts,
1967 + );
1968 +
1969 + // Variant exists for the product variant. LP 2026-07-02
1970 + $button_args = $args;
1971 + if (isset($button_args['button_variant'])) $button_args['variant'] = $button_args['button_variant'];
1972 + unset($button_args['button_variant']);
1973 + unset($button_args['sku']);
1974 + unset($button_args['id']);
1975 + // NB: the language may be incorrect for the shortcode, see web component bug at https://developer.vippsmobilepay.com/docs/knowledge-base/buttons/
1976 + // "Note also that there is a bug in the library, and it currently only renders one language per page."
1977 + // 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
1978 +
1979 + return "<div class='vipps_buy_now_wrapper noloop'>". $this->get_buy_now_button($args['id'], $args['variant'], $args['sku'], false, '', 'shortcode', $button_args) . "</div>";
1612 1980 }
1613 1981
1614 1982 // 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
1615 1983 public function express_checkout_button_shortcode() {
@@ -1615,9 +1983,9 @@
1615 1983 public function express_checkout_button_shortcode() {
1616 1984 $gw = $this->gateway();
1617 1985 if (!$gw->cart_supports_express_checkout()) return;
1618 1986 ob_start();
1619 - $this->cart_express_checkout_button_html('shortcode');
1987 + $this->cart_express_checkout_button_html('cart');
1620 1988 return ob_get_clean();
1621 1989 }
1622 1990 // Show a banner normally shown for non-logged-in-users at the checkout page. It does not need to check if we are to show the button, obviously, but needs to see if the cart works
1623 1991 public function express_checkout_banner_shortcode() {
@@ -1885,9 +2253,9 @@
1885 2253 public function add_vipps_metabox ($post_or_order_object) {
1886 2254 $order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object;
1887 2255 $order = wc_get_order($post_or_order_object);
1888 2256 $pm = $order->get_payment_method();
1889 - if ($pm != 'vipps') return;
2257 + if (!self::is_vipps_order($pm)) return;
1890 2258 $orderid=$order->get_id();
1891 2259
1892 2260 $init = intval($order->get_meta('_vipps_init_timestamp'));
1893 2261 $callback = intval($order->get_meta('_vipps_callback_timestamp'));
@@ -1983,9 +2351,9 @@
1983 2351 print "<p>" . __("Unknown order", 'woo-vipps') . "</p>";
1984 2352 exit();
1985 2353 }
1986 2354 $pm = $order->get_payment_method();
1987 - if ($pm != 'vipps') {
2355 + if (!self::is_vipps_order($pm)) {
1988 2356 print "<p>" . sprintf(__("The order is not a %1\$s order", 'woo-vipps'), $this->get_payment_method_name()) . "</p>";
1989 2357 exit();
1990 2358 }
1991 2359
@@ -2229,24 +2597,28 @@
2229 2597
2230 2598
2231 2599 // Because the prefix used to create the Vipps order id is editable
2232 2600 // by the user, we will store that as a meta and use this for callbacks etc.
2233 - // IOK: This needs to be replaced by a separate table, but in the meantime, we will use
2234 - // wc_get_orders and not $wpdb directly, so it should work with HPOS too.
2235 2601 // IOK 2023-01-23 this function is no longer used, and kept only for backwards compatibility with
2236 2602 // debug filters and similar.
2603 + // IOK 2026-05-27 rewritten to avoid wc_get_orders for pre-HPOS. Still not used.
2237 2604 public function getOrderIdByVippsOrderId($vippsorderid) {
2238 - // Ensure the old order table understands the meta query IOK 2022-12-02
2239 - static::add_wc_order_meta_key_support();
2240 - $result = wc_get_orders( array(
2241 - 'limit' => 1,
2242 - 'return' => 'ids',
2243 - 'meta_vipps_orderid' => $vippsorderid,
2244 - /* The above, with the filter, is for the old orders table, the below is for the new IOK 2022-12-02 */
2245 - 'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]]
2246 - ));
2247 - if ($result && is_array($result)) return $result[0];
2248 -
2605 + $result = false;
2606 + if ($this->useHPOS()) {
2607 + $result = wc_get_orders( array(
2608 + 'limit' => 1,
2609 + 'return' => 'ids',
2610 + 'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]]
2611 + ));
2612 + if ($result && is_array($result)) return $result[0];
2613 + } else {
2614 + // Pre-HPOS did not support meta_query, so we're doing it with direct access to the database. IOK 2026-05-27
2615 + global $wpdb;
2616 + $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);
2617 + $res = $wpdb->get_results($q, ARRAY_A);
2618 + if (empty($res)) return 0;
2619 + return $res[0]['ID'];
2620 + }
2249 2621 return 0;
2250 2622 }
2251 2623
2252 2624 // This is like getOrderByVipsOrderId, but only fetches pending orders.
@@ -2257,9 +2629,8 @@
2257 2629 $result = wc_get_orders( array(
2258 2630 'limit' => 1,
2259 2631 'status' => 'wc-pending',
2260 2632 'type' => 'shop_order',
2261 - 'payment_method' => 'vipps',
2262 2633 'date_created' => '>' . $sevendaysago,
2263 2634 'return' => 'objects',
2264 2635 'meta_query' => [[ 'key' => '_vipps_orderid', 'value' => $vippsorderid ]]
2265 2636 ));
@@ -2275,77 +2646,87 @@
2275 2646 return null;
2276 2647 }
2277 2648 }
2278 2649
2650 + // Special pages, and some callbacks. IOK 2018-05-18
2651 + public function template_redirect() {
2279 2652
2280 - // If this is a special page, return true very early because we are handling this. IOK 2023-02-22
2281 - public function pre_handle_404($current, $query) {
2282 - if (!is_admin()) {
2283 - $special = $this->is_special_page();
2284 - if ($special) {
2285 - // Ensure very early on that Autooptimize does not try to optimize us (if installed) IOK 2023-03-04
2286 - add_filter( 'autoptimize_filter_noptimize', '__return_true');
2287 - return true;
2288 - }
2653 + // Handle legacy vipps-buy-now urls that auto-start express checkout for certain product - in QR codes etc IOK 2026-09-11
2654 + // We redirect these to the new location.
2655 + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
2656 + if (( ($_GET['VippsSpecialPage'] ?? '') == 'vipps-buy-product') || ($path && preg_match("!/vipps-buy-product/?$!", $path)) ) {
2657 + $url = static::get_special_page_url();
2658 + $_GET['action'] = 'buy_product';
2659 + $q = build_query($_GET);
2660 + wp_redirect($url . "?" . $q, 302);
2661 + exit();
2289 2662 }
2290 - return $current;
2663 +
2664 + if (static::is_special_page()) {
2665 + // Legacy: Stop the canonical redirect here. Unclear if still necessary. IOK 2026-09-11
2666 + remove_filter('template_redirect', 'redirect_canonical', 10);
2667 + // dont cache special page. LP 2026-08-25
2668 + $this->nocache();
2669 + // Do the custom pre-load actions for these pages IOK 2026-09-11
2670 + do_action('woo_vipps_before_handling_special_page', ($_GET['action'] ?? ""));
2671 + }
2291 2672 }
2292 2673
2293 - // Special pages, and some callbacks. IOK 2018-05-18
2294 - public function template_redirect() {
2295 - global $post;
2296 - // Handle special callbacks
2297 - $special = $this->is_special_page() ;
2674 + // Ran in template redirect for the special page. IOK 2026-09-2
2675 + public function pre_special_page_actions ($action) {
2676 + // Change title dynamically depending on action. LP 2026-09-02
2677 + add_filter('the_title', [$this, 'vipps_special_page_endpoint_title'], 10, 2);
2298 2678
2299 - if ($special) {
2300 - remove_filter('template_redirect', 'redirect_canonical', 10);
2301 - do_action('woo_vipps_before_handling_special_page', $special);
2679 + // If we are handling the 'wait for payment' action, we need to poll the order status before
2680 + // we start producing content IOK 2026-09-21
2681 + if ($action == 'wait_for_payment') {
2682 + $this->handle_payment_poll_and_redirect();
2683 + }
2302 2684
2303 - // Allow above hook to actually handle special pages. It should probably call $Vipps->fakepage or a redirect; can be used
2304 - // to intercept express checkout etc. IOK 2022-03-18
2305 - if (! apply_filters('woo_vipps_special_page_handled', false, $special)) {
2306 - $this->$special();
2307 - }
2685 + // Some validation is required for this action
2686 + if ($action == 'do_express_checkout') {
2687 + $this->vipps_express_checkout_consistency_check();
2308 2688 }
2689 + }
2309 2690
2310 - $consentremoval = $this->is_consent_removal();
2311 - if ($consentremoval) {
2312 - remove_filter('template_redirect', 'redirect_canonical', 10);
2313 - do_action('woo_vipps_before_handling_special_page', 'consentremoval');
2314 - if (! apply_filters('woo_vipps_special_page_handled', false, 'consentremoval')) {
2315 - $this->vipps_consent_removal_callback($consentremoval);
2691 +
2692 + // Dynamic special page title depending on endpoint/action, only frontend. LP 2026-09-02
2693 + public function vipps_special_page_endpoint_title($title, $postid = 0) {
2694 + global $wp_query;
2695 + // Comment from woocommerce's wc_page_endpoint_title where this logic is from: LP 2026-09-02
2696 +
2697 + // In block themes the whole template (header, footer, content) renders inside the main
2698 + // loop, so `the_title` fires for any post title rendered on the page (e.g. a product in a
2699 + // server-rendered mini-cart) - not just the page's own heading. Only replace the title of
2700 + // the queried page so an earlier title doesn't consume this one-shot filter.
2701 + if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && in_the_loop() && is_page() && $postid == static::get_special_page_id() ) {
2702 + switch ($_GET['action'] ?? '') {
2703 + case 'wait_for_payment':
2704 + $title = __('Processing order', 'woo-vipps');
2705 + break;
2706 + case 'do_express_checkout':
2707 + case 'buy_product':
2708 + $title = __('Express Checkout', 'woo-vipps');
2709 + break;
2316 2710 }
2317 2711 }
2712 + return $title;
2318 2713 }
2714 +
2319 2715 // Template handling for special pages. IOK 2018-11-21
2716 + // 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
2320 2717 public function template_include($template) {
2321 - $special = $this->is_special_page() ;
2322 - if ($special) {
2718 + if (static::is_special_page()) {
2323 2719 // Get any special template override from the options IOK 2020-02-18
2324 2720 $specific = $this->gateway()->get_option('vippsspecialpagetemplate');
2325 2721 $found = locate_template($specific,false,false);
2326 2722 if ($found) $template=$found;
2327 2723
2328 - return apply_filters('woo_vipps_special_page_template', $template, $special);
2724 + return apply_filters('woo_vipps_special_page_template', $template, $_GET['action'] ?? '');
2329 2725 }
2330 2726 return $template;
2331 2727 }
2332 2728
2333 -
2334 - // Can't use wc-api for this, as that does not support DELETE . IOK 2018-05-18
2335 - private function is_consent_removal () {
2336 -
2337 - if ($_SERVER['REQUEST_METHOD'] != 'DELETE') return false;
2338 - if ( !get_option('permalink_structure')) {
2339 - if (@$_REQUEST['vipps-consent-removal']) return @$_REQUEST['callback'];
2340 - return false;
2341 - }
2342 - if (preg_match("!/vipps-consent-removal/([^/]*)!", $_SERVER['REQUEST_URI'], $matches)) {
2343 - return @$_REQUEST['callback'];
2344 - }
2345 - return false;
2346 - }
2347 -
2348 2729 // On the thank you page, we have a completed order, so we need to restore any saved cart and possibly log in
2349 2730 // the user if using Express Checkout IOK 2020-10-09
2350 2731 public function woocommerce_before_thankyou ($orderid) {
2351 2732 $order = wc_get_order($orderid);
@@ -2350,9 +2731,9 @@
2350 2731 public function woocommerce_before_thankyou ($orderid) {
2351 2732 $order = wc_get_order($orderid);
2352 2733 if ($order) {
2353 2734 // Requires that this is express checkout and that 'create users on express checkout' is chosen. IOK 2020-10-09
2354 - // -- 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
2735 + // -- or the same thing for Checkout. Also, the NHG code should not be running, and there is a filter, too. IOK 2023-08-04
2355 2736 $this->maybe_log_in_user($order);
2356 2737 $order->delete_meta_data('_vipps_limited_session');
2357 2738 $order->save();
2358 2739
@@ -2384,9 +2765,9 @@
2384 2765
2385 2766 // If local pickup has been added to express/checkout by filters, add this to emails/confirmation pages. IOK 2025-08-15
2386 2767 add_filter('woocommerce_order_shipping_to_display', function($shipping, $order, $tax_display) {
2387 2768 if (!is_a($order, 'WC_Order')) return $shipping;
2388 - if ($order->get_payment_method() != 'vipps') return $shipping;
2769 + if (! self::is_vipps_order($order)) return $shipping;
2389 2770 $shipping_method = current( $order->get_shipping_methods() );
2390 2771
2391 2772 if (empty($shipping_method)) return $shipping;
2392 2773
@@ -2413,9 +2794,8 @@
2413 2794
2414 2795 // Support adding pickup locations to any shipping rate using the 'woo_vipps_shipping_method_pickup_points' filter
2415 2796 // IOK 2025-11-19
2416 2797 add_filter('woo_vipps_modify_express_checkout_rate', array($this, 'express_add_pickup_location_options'), 10, 4);
2417 -
2418 2798 }
2419 2799
2420 2800 public function get_payment_method_name() {
2421 2801 return $this->gateway()->get_option('payment_method_name');
@@ -2435,14 +2815,13 @@
2435 2815 public function after_setup_theme() {
2436 2816 // To facilitate development, allow loading the plugin-supplied translations. Must be called here at the earliest.
2437 2817 $ok = Vipps::load_plugin_textdomain('woo-vipps', false, basename( dirname( dirname( __FILE__ ) ) ) . "/languages");
2438 2818
2439 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2819 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
2440 2820 // Will also probably be used to maintain a real utility-page for Vipps actions later for themes where this
2441 2821 // is important.
2442 2822 add_filter('woocommerce_create_pages', array($this, 'woocommerce_create_pages'), 50, 1);
2443 2823
2444 -
2445 2824 // Callbacks use the Woo API IOK 2018-05-18
2446 2825 add_action( 'woocommerce_api_wc_gateway_vipps', array($this,'vipps_callback'));
2447 2826 add_action( 'woocommerce_api_vipps_shipping_details', array($this,'vipps_shipping_details_callback'));
2448 2827
@@ -2453,9 +2832,9 @@
2453 2832 add_action( 'woocommerce_cart_actions', array($this, 'cart_express_checkout_button'));
2454 2833 add_action( 'woocommerce_widget_shopping_cart_buttons', array($this, 'minicart_express_checkout_button'), 30);
2455 2834
2456 2835 // Previously we added an express html banner to the action 'woocommerce_before_checkout_form.',
2457 - // replaced by the new express buttons in manner more like Gutenberg. LP 2026-03-23
2836 + // replaced by the new express buttons in manner more like Gutenberg. for grepping: "express legacy checkout". LP 2026-03-23
2458 2837 add_action('woocommerce_checkout_before_customer_details', array($this, 'checkout_before_customer_details_express'), 5);
2459 2838
2460 2839 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2461 2840 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
@@ -2460,12 +2839,10 @@
2460 2839 add_action('woocommerce_after_add_to_cart_button', array($this, 'single_product_buy_now_button'));
2461 2840 add_action('woocommerce_after_shop_loop_item', array($this, 'loop_single_product_buy_now_button'), 20);
2462 2841
2463 2842
2464 - // Special pages and callbacks handled by template_redirect
2465 - // We must also notify WP and other plugins that we are handling this 404-like situation. IOK 2023-02-22
2843 + // Special pages and callbacks handled by template_redirect. IOK 2023-02-22
2466 2844 add_action('template_redirect', array($this,'template_redirect'),1);
2467 - add_action('pre_handle_404', array($this, 'pre_handle_404'), 1, 2);
2468 2845
2469 2846 // Allow overriding their templates
2470 2847 add_filter('template_include', array($this,'template_include'), 10, 1);
2471 2848
@@ -2495,12 +2872,12 @@
2495 2872 // If we can't cancel for some other reason, don't.
2496 2873 if (!$cancel) return $cancel;
2497 2874
2498 2875 // Only check Vipps orders
2499 - if ($order->get_payment_method() != 'vipps') return $cancel;
2876 + if (! self::is_vipps_order($order)) return $cancel;
2500 2877
2501 - // For Vipps, all unpaid orders must be pending. IOK FIXME ADD FAILED
2502 - if ($order->get_status() != 'pending') return $cancel;
2878 + // For Vipps, all unpaid orders must be pending.
2879 + if ($order->get_status() != 'pending' && $order->get_status() != 'failed') return $cancel;
2503 2880
2504 2881 // Handle this separately, in the Checkout class. IOK 2025-10-08
2505 2882 $checkout_session = $order->get_meta('_vipps_checkout_session');
2506 2883 if ($checkout_session) {
@@ -2572,15 +2949,21 @@
2572 2949 $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());
2573 2950 $this->vippsJSConfig['vippslanguage'] = $this->get_customer_language();
2574 2951 $this->vippsJSConfig['vippslocale'] = get_locale();
2575 2952 $this->vippsJSConfig['vippsexpressbuttonurl'] = $this->get_payment_method_name();
2576 - $this->vippsJSConfig['logoSvgUrl'] = $this->get_payment_logo('buy-now-block');
2577 2953
2578 2954
2579 2955 // If the site supports Gutenberg Blocks, support the Checkout block IOK 2020-08-10
2580 2956 if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) {
2957 + // Ensure gateways are loaded at this point IOK 2026-05-27
2958 + require_once(dirname(__FILE__) . '/WC_Gateway_VippsCard.class.php');
2959 + require_once(dirname(__FILE__) . '/WC_Gateway_Vipps.class.php');
2960 +
2961 + // Then the payment blocks
2581 2962 require_once(dirname(__FILE__) . "/Blocks/Payment/Vipps.class.php");
2963 + require_once(dirname(__FILE__) . "/Blocks/Payment/VippsCard.class.php");
2582 2964 Automattic\WooCommerce\Blocks\Payments\Integrations\Vipps::register();
2965 + Automattic\WooCommerce\Blocks\Payments\Integrations\VippsCard::register();
2583 2966 }
2584 2967
2585 2968 // Used for e.g. labels of product/shipping metadata. IOK 2025-05-07
2586 2969 add_filter('woocommerce_attribute_label', function ($label, $name, $product) {
@@ -2636,9 +3019,9 @@
2636 3019 static::set_locale_if_in_header();
2637 3020 $order = wc_get_order(intval($_REQUEST['orderid']));
2638 3021 if (!is_a($order, 'WC_Order')) return;
2639 3022 $pm = $order->get_payment_method();
2640 - if ($pm != 'vipps') return;
3023 + if (!self::is_vipps_order($pm)) return;
2641 3024
2642 3025 $action = isset($_REQUEST['do']) ? sanitize_title($_REQUEST['do']) : 'none';
2643 3026
2644 3027 if ($action == 'do_capture') {
@@ -2707,9 +3090,9 @@
2707 3090 }
2708 3091
2709 3092 public function order_item_add_capture_button ($order) {
2710 3093 $pm = $order->get_payment_method();
2711 - if ($pm != 'vipps') return;
3094 + if (!self::is_vipps_order($pm)) return;
2712 3095 $status = $order->get_status();
2713 3096
2714 3097 $show_capture_button = ($status == 'on-hold' || $status == 'processing');
2715 3098 if (!apply_filters('woo_vipps_show_capture_button', $show_capture_button, $order)) {
@@ -2716,9 +3099,10 @@
2716 3099 return;
2717 3100 }
2718 3101
2719 3102 $captured = intval($order->get_meta('_vipps_captured'));
2720 - $capremain = intval($order->get_meta('_vipps_capture_remaining'));
3103 + // noncapturable should never be greater than capture remaining, so this *should* not be negative. LP 2026-06-12
3104 + $capremain = intval($order->get_meta('_vipps_capture_remaining')) - intval($order->get_meta('_vipps_noncapturable'));
2721 3105 if ($captured && (!$capremain || $capremain < 2)) {
2722 3106 print "<div><strong>" . sprintf(__("The entire amount has been captured at %1\$s", 'woo-vipps'), $this->get_payment_method_name()) . "</strong></div>";
2723 3107 return;
2724 3108 }
@@ -2744,14 +3128,14 @@
2744 3128
2745 3129 $raw_post = @file_get_contents( 'php://input' );
2746 3130 $result = @json_decode($raw_post,true);
2747 3131
2748 - // This handler handles both Vipps Checkout and Vipps ECom IOK 2021-09-02
3132 + // This handler handles both Checkout and Vipps ECom IOK 2021-09-02
2749 3133 // .. and the epayment webhooks 2023-12-19
2750 3134 $ischeckout = false;
2751 3135 $iswebhook = false;
2752 3136 $callback = isset($_REQUEST['callback']) ? $_REQUEST['callback'] : "";
2753 - // For Vipps Checkout v3 and onwards, we control the callback so the type is just this field
3137 + // For Checkout v3 and onwards, we control the callback so the type is just this field
2754 3138 if ($callback == 'checkout') {
2755 3139 $ischeckout = true;
2756 3140 }
2757 3141 // For the webhooks, we will add 'webhook' to the result, but we also know that 'pspReference' will be present. IOK 2023-12-19
@@ -3145,9 +3529,9 @@
3145 3529 print "Unknown order";
3146 3530 $this->log(__('Could not find Woo order with id:', 'woo-vipps') . " " . $orderid, 'error');
3147 3531 exit();
3148 3532 }
3149 - if ($order->get_payment_method() != 'vipps') {
3533 + if (!self::is_vipps_order($order)) {
3150 3534 status_header(400, "Invalid order");
3151 3535 print "Invalid order";
3152 3536 $this->log(__('Invalid order for shipping callback:', 'woo-vipps') . " " . $orderid, 'error');
3153 3537 exit();
@@ -3165,10 +3549,10 @@
3165 3549 $this->log(sprintf(__("Wrong %1\$s Orderid on shipping details callback", 'woo-vipps'), $this->get_payment_method_name()), 'warning');
3166 3550 exit();
3167 3551 }
3168 3552
3169 - // If we are doing this for Vipps Checkout after version 3, communicate to any shipping methods with
3170 - // special support for Vipps Checkout that this is in fact happening. IOK 2023-01-19
3553 + // If we are doing this for Checkout after version 3, communicate to any shipping methods with
3554 + // special support for Checkout that this is in fact happening. IOK 2023-01-19
3171 3555 // This needs to be done before "calculate totals".
3172 3556 // Moved from "vipps_shipping_details_callback_handler" because we need it before restoring sessions. IOK 2025-05-06
3173 3557 $ischeckout = $order->get_meta('_vipps_checkout');
3174 3558
@@ -3344,9 +3728,9 @@
3344 3728 ), 'debug');
3345 3729
3346 3730 }
3347 3731
3348 - // Add shipping tax rates to the *order* so we can calculate this correctly when using Vipps Checkouts
3732 + // Add shipping tax rates to the *order* so we can calculate this correctly when using Checkouts
3349 3733 // 'dynamic pricing' 2023-01-26
3350 3734 // Which may be deprecated, but anyway, for future use IOK 2025-08-14
3351 3735 $taxrate = 0;
3352 3736 if (is_array($shipping_tax_rates) && !empty($shipping_tax_rates)) {
@@ -3472,9 +3856,9 @@
3472 3856 $vippsmethod['shippingMethod'] = $rate->get_label();
3473 3857 $vippsmethod['shippingMethodId'] = $key;
3474 3858 $vippsmethods[]=$vippsmethod;
3475 3859
3476 - // Metadata and settings stored for later use for Vipps Checkout
3860 + // Metadata and settings stored for later use for Checkout
3477 3861 // and express checkout - basically, for each *key* have the corresponding object. IOK 2025-08-15
3478 3862 // In the end, this data will be serialized and stored in the Order, and used in the gateways method set_order_shipping_details to
3479 3863 // finalize the order. IOK 2025-08-15
3480 3864 $ratemap[$key]=$rate;
@@ -3492,9 +3876,9 @@
3492 3876 // This then is the old Express Checkout format, which we have exposed in filters. IOK 2025-08-14
3493 3877 $return = array('addressId'=>intval($addressid), 'orderId'=>$vippsorderid, 'shippingDetails'=>$vippsmethods);
3494 3878 $return = apply_filters('woo_vipps_vipps_formatted_shipping_methods', $return); // Mostly for debugging
3495 3879
3496 - // IOK 2021-11-16 Vipps Checkout uses a slightly different syntax and format.
3880 + // IOK 2021-11-16 Checkout uses a slightly different syntax and format.
3497 3881 // IOK 2025-08-15 and new Express yet another slightly different format.
3498 3882 // IOK 2025-08-15 pass the ratemap as a reference, so transforms can update them
3499 3883 if ($ischeckout) {
3500 3884 $return = VippsCheckout::instance()->format_shipping_methods($return, $ratemap, $methodmap, $order);
@@ -3784,9 +4168,9 @@
3784 4168 WC()->cart->calculate_totals();
3785 4169 WC()->cart->set_session();
3786 4170 return true;
3787 4171 } catch (Exception $e) {
3788 - $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->get_message()), 'error');
4172 + $this->log(sprintf(__("Error regenerating cart from order %1\$d: %2\$s", 'woo-vipps'), $order_id, $e->getMessage()), 'error');
3789 4173 return false;
3790 4174 }
3791 4175 }
3792 4176
@@ -3870,19 +4254,11 @@
3870 4254 header("X-Accel-Expires: 0");
3871 4255 }
3872 4256
3873 4257
3874 -
3875 - // Handle DELETE on a vipps consent removal callback
3876 - public function vipps_consent_removal_callback ($callback) {
3877 - Vipps::nocache();
3878 - // Currently, no such requests will be posted, and as this code isn't sufficiently tested,we'll just have
3879 - // to escape here when the API is changed. IOK 2020-10-14
3880 - $this->log("Consent removal is non-functional pending API changes as of 2020-10-14"); print "1"; exit();
3881 - }
3882 -
3883 4258 public function woocommerce_payment_gateways($methods) {
3884 4259 require_once(dirname(__FILE__) . "/WC_Gateway_Vipps.class.php");
4260 + require_once(dirname(__FILE__) . "/WC_Gateway_VippsCard.class.php");
3885 4261 // Protect the singleton: Use the object instead of the class name IOK 2025-02-04
3886 4262 $gateway = $this->gateway();
3887 4263 if ($gateway) {
3888 4264 $methods[] = $gateway;
@@ -3888,8 +4264,11 @@
3888 4264 $methods[] = $gateway;
3889 4265 } else {
3890 4266 $methods[] = 'WC_Gateway_Vipps';
3891 4267 }
4268 +
4269 + $methods[] = 'WC_Gateway_VippsCard';
4270 +
3892 4271 return $methods;
3893 4272 }
3894 4273
3895 4274 // Runs after set_session, so if the session is just created, we'll get called. IOK 2018-06-06
@@ -3901,9 +4280,11 @@
3901 4280 if ( empty($_REQUEST['add-to-cart']) || ! is_numeric($_REQUEST['add-to-cart']) || empty($_REQUEST['vipps_compat_mode']) || !$_REQUEST['vipps_compat_mode']) {
3902 4281 return $url;
3903 4282 }
3904 4283 $url = $this->express_checkout_url();
3905 - $url = wp_nonce_url($url,'express','sec');
4284 + // At this point, there is always a query argument here. IOK 2026-09-21
4285 + $nonce = wp_create_nonce('express');
4286 + $url = $url . "&sec=$nonce";
3906 4287
3907 4288 return $url;
3908 4289 }
3909 4290
@@ -3912,9 +4293,9 @@
3912 4293 // But for express checkout this breaks because there is no shipping method or address, and of course,
3913 4294 // the order id is unique too.. IOK 2018-11-21
3914 4295 public function woocommerce_my_account_my_orders_actions($actions, $order ) {
3915 4296 $pm = $order->get_payment_method();
3916 - if ($pm != 'vipps') return $actions;
4297 + if (!self::is_vipps_order($pm)) return $actions;
3917 4298
3918 4299 if (!static::order_is_vipps_retryable($order->get_id())) {
3919 4300 unset($actions['pay']);
3920 4301 }
@@ -3932,10 +4313,14 @@
3932 4313 // to the store and the Vipps callback fails for whatever reason. IOK 2021-06-21
3933 4314 public function cron_check_for_missing_callbacks() {
3934 4315 $eightminutesago = time() - (60*8);
3935 4316 $sevendaysago = time() - (60*60*24*7);
3936 - $pending = wc_get_orders(
3937 - array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps', 'date_created' => '>' . $sevendaysago ));
4317 +
4318 + // This is compatible with both HPOS and old style order management. IOK 2026-05-27
4319 + $pending_app = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps', 'date_created' => '>' . $sevendaysago ));
4320 + $pending_cards = wc_get_orders( array('limit'=>-1, 'status'=>'pending', 'payment_method' => 'vipps_card', 'date_created' => '>' . $sevendaysago ));
4321 + $pending = array_merge($pending_app, $pending_cards);
4322 +
3938 4323 if (empty($pending)) return;
3939 4324 foreach ($pending as $o) {
3940 4325 $then = $o->get_meta('_vipps_init_timestamp');
3941 4326 if (! $then) continue; # Race condition! We may not have set the timestamp yet. IOK 2022-03-24
@@ -3947,9 +4332,9 @@
3947 4332 if ($currentstatus != 'initiated') {
3948 4333 $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');
3949 4334 return;
3950 4335 }
3951 - $this->check_status_of_pending_order($o, false, false);
4336 + $this->check_status_of_pending_order($o, false);
3952 4337 }
3953 4338 }
3954 4339
3955 4340 // 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 -
@@ -3954,30 +4339,35 @@
3954 4339
3955 4340 // 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 -
3956 4341 // e.g. wp-cron. IOK 2021-06-21
3957 4342 // Stop restoring session in wp-cron too. IOK 2021-08-23
3958 - public function check_status_of_pending_order($order, $maybe_restore_session=0, $allow_retry=true) {
3959 - $express = $order->get_meta('_vipps_express_checkout');
3960 - $vippstatus = $order->get_meta('_vipps_status');
3961 - if ($express && $maybe_restore_session) {
3962 - $this->log(sprintf(__("Restoring session of order %1\$d", 'woo-vipps'), $order->get_id()), 'debug');
3963 - $this->callback_restore_session($order->get_id());
3964 - }
4343 + // Stop restoring session in wp-cron again(?) since we now use a rest endpoint to handle shipping. LP 2026-05-13
4344 + public function check_status_of_pending_order($order, $allow_retry=true) {
3965 4345 $gw = $this->gateway();
3966 4346
3967 4347 $order_status = null;
3968 4348 try {
3969 4349 $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()));
3970 - $order_status = $gw->callback_check_order_status($order, $allow_retry);
4350 +
4351 + // Poll status and correct woo status. LP 2026-05-19
4352 + $order_data = $gw->get_payment_details($order);
4353 +
4354 + // If we already know the order failed, we don't need to process the order further below. LP 2026-05-19
4355 + if ('CANCEL' === ($order_data['state'] ?? "")) {
4356 + /* translators: company name */
4357 + $order->update_status('cancelled', sprintf(__('Payment cancelled at %1$s.', 'woo-vipps'), Vipps::CompanyName()));
4358 + return;
4359 + }
4360 +
4361 + $gw->set_order_status_by_payment_details($order, $order_data, $allow_retry);
4362 + $order = wc_get_order($order->get_id()); // refresh order if changed. LP 2026-05-13
4363 + $order_status = $order->get_status();
4364 +
3971 4365 $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');
3972 4366 } catch (Exception $e) {
3973 4367 $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');
3974 4368 $this->log($e->getMessage() . "\n" . $order->get_id(), 'error');
3975 4369 }
3976 - // Ensure we don't keep using an old session for more than one order here.
3977 - if ($express && $maybe_restore_session) {
3978 - $this->callback_destroy_session();
3979 - }
3980 4370 return $order_status;
3981 4371 }
3982 4372
3983 4373 // 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
@@ -3990,20 +4380,40 @@
3990 4380 }
3991 4381 }
3992 4382
3993 4383 public function activate () {
3994 - static::maybe_add_cron_event();
3995 - $gw = $this->gateway();
4384 + static::maybe_add_cron_event();
4385 + $gw = $this->gateway();
3996 4386
3997 - // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
3998 - if ($gw->get_option('orderprefix') == 'Woo') {
3999 - $gw->update_option('orderprefix', $this->generate_order_prefix());
4000 - }
4001 - // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4002 - $gw->initialize_webhooks();
4003 - $this->payment_method_name = $gw->get_option('payment_method_name');
4004 - }
4387 + // If store is using the default "Woo" orderprefix, generate a new one, this time using the stores' sitename if possible. IOK 2020-05-19
4388 + if ($gw->get_option('orderprefix') == 'Woo') {
4389 + $gw->update_option('orderprefix', $this->generate_order_prefix());
4390 + }
4391 + // IOK 2023-12-20 for the epayment api, we need to re-initialize webhooks at this point.
4392 + $gw->initialize_webhooks();
4393 + $this->payment_method_name = $gw->get_option('payment_method_name');
4005 4394
4395 +
4396 + // Check if the special page is noted and actually does exist
4397 + $special = static::get_special_page_id();
4398 + if ($special) {
4399 + $special_page = get_post($special);
4400 + if ($special_page && 'trash' !== $special_page->post_status) {
4401 + // Ensure this page has the necessary shortcode. LP 2026-09-01
4402 + if (!has_shortcode($special_page->post_content, 'vipps_special_page')) {
4403 + $new_content = $special_page->post_content . "\n\n<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->";
4404 + wp_update_post([
4405 + 'ID' => $special,
4406 + 'post_content' => $new_content,
4407 + ]);
4408 + }
4409 + } else {
4410 + delete_option('woocommerce_vipps_special_page_page_id');
4411 + }
4412 + }
4413 +
4414 + }
4415 +
4006 4416 // We have added some hooks to wp-cron; remove these. IOK 2020-04-01
4007 4417 public static function deactivate() {
4008 4418 $timestamp = wp_next_scheduled('vipps_cron_cleanup_hook');
4009 4419 wp_unschedule_event($timestamp, 'vipps_cron_cleanup_hook');
@@ -4014,13 +4424,12 @@
4014 4424 $gw->delete_all_webhooks();
4015 4425
4016 4426 // Delete all settings if checked in settings menu. LP 2025-10-06
4017 4427 $should_delete = $gw->get_option( 'delete_settings_on_deactivation' ) === 'yes';
4018 - if ( ($should_delete)) {
4428 + if ($should_delete) {
4019 4429 // Delete options.
4020 - $options = ['woocommerce_vipps_settings', 'woo-vipps-configured', 'vipps_badge_options', 'vipps_button_options', '_vipps_dismissed_notices', 'woo_vipps_checkout_activated'];
4430 + $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'];
4021 4431 foreach($options as $option) {
4022 - error_log("Deleting woo-vipps option: $option");
4023 4432 delete_option($option);
4024 4433 }
4025 4434 }
4026 4435
@@ -4057,9 +4466,10 @@
4057 4466 // If setting is true, use Vipps as default payment. Called by the woocommrece_cart_updated hook. IOK 2018-06-06
4058 4467 private function maybe_set_vipps_as_default() {
4059 4468 if (WC()->session->get('chosen_payment_method')) return; // User has already chosen payment method, so we're done.
4060 4469 $gw = $this->gateway();
4061 - if ($gw->get_option('vippsdefault')=='yes') {
4470 + // Do *not* default to vipps if Kustom Checkout is installed IOK 2026-09-11
4471 + if ($gw->get_option('vippsdefault')=='yes' && !class_exists('KCO')) {
4062 4472 WC()->session->set('chosen_payment_method', $gw->id);
4063 4473 }
4064 4474 }
4065 4475
@@ -4161,16 +4571,16 @@
4161 4571 // Maybe log in user
4162 4572 // It is done on the thank-you page of the order, and only for express checkout.
4163 4573 function maybe_log_in_user ($order) {
4164 4574 if (is_user_logged_in()) return;
4165 - if (!$order || $order->get_payment_method()!= 'vipps' ) return;
4575 + if (!$order || ! self::is_vipps_order($order)) return;
4166 4576
4167 4577 // We *do* want to log in express checkout customers, but not those that
4168 - // use the Vipps Checkout solution - those can change their emails in the
4578 + // use the Checkout solution - those can change their emails in the
4169 4579 // checkout screen. IOK 2021-09-03
4170 4580 $do_login = $order->get_meta('_vipps_express_checkout');
4171 4581
4172 - // We will not log in Vipps Checkout users unless the option for that is true
4582 + // We will not log in Checkout users unless the option for that is true
4173 4583 if ($order->get_meta('_vipps_checkout') && 'yes' != $this->gateway()->get_option('checkoutcreateuser')) {
4174 4584 $do_login = false;
4175 4585 }
4176 4586
@@ -4198,9 +4608,9 @@
4198 4608
4199 4609 // Get the customer that corresponds to the current order, maybe creating the customer if it does not exist yet and
4200 4610 // the settings allow it.
4201 4611 function express_checkout_get_vipps_customer($order) {
4202 - if (!$order || $order->get_payment_method() != 'vipps' ) return null;
4612 + if (!$order || ! self::is_vipps_order($order)) return null;
4203 4613 // specific code for this by netthandelsgruppen if the below function exists
4204 4614 if (function_exists('create_assign_user_on_vipps_callback')) return null;
4205 4615
4206 4616 // Both Checkout and Express Checkout have the below value set to true
@@ -4205,9 +4615,9 @@
4205 4615
4206 4616 // Both Checkout and Express Checkout have the below value set to true
4207 4617 if (!$order->get_meta('_vipps_express_checkout')) return;
4208 4618
4209 - // Creating/logging in users are handled separately for Vipps Checkout and Express Checkout, so check the correct setting
4619 + // Creating/logging in users are handled separately for Checkout and Express Checkout, so check the correct setting
4210 4620 // IOK 2023-07-27
4211 4621 $ischeckout = $order->get_meta('_vipps_checkout');
4212 4622 if ($ischeckout) {
4213 4623 if ($this->gateway()->get_option('checkoutcreateuser') != 'yes') return null;
@@ -4307,9 +4717,10 @@
4307 4717 }
4308 4718 if (!$o) return;
4309 4719 if (!$o->get_meta('_vipps_single_product_express')) return;
4310 4720 if ($failed && !apply_filters('woo_vipps_restore_cart_on_express_checkout_failure', true, $o)) return;
4311 - if ($failed) WC()->cart->empty_cart();
4721 + // Restoring cart! But clear it first so we dont add this single product to the restored cart. LP 2026-09-22
4722 + WC()->cart->empty_cart();
4312 4723 $this->restore_cart($o);
4313 4724 }
4314 4725
4315 4726
@@ -4592,9 +5003,9 @@
4592 5003 $ok = wc()->shipping->register_shipping_method( new Automattic\WooCommerce\Blocks\Shipping\PickupLocation() );
4593 5004 }
4594 5005 }
4595 5006
4596 - // Vipps Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
5007 + // Checkout and Express Checkout allows loading specific kinds of shipping methods with non-standard APIs, such as PickupLocations. IOK 2025-05-08
4597 5008 // Must be called *early*. IOK 2025-05-08. Called in callback methods, and if using static shipping, in the 'start session' callback.
4598 5009 public function load_extra_shipping_methods($order, $addressdata, $ischeckout=false) {
4599 5010 // 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
4600 5011 add_action('woocommerce_load_shipping_methods', function () use ($order, $addressdata) {
@@ -4665,46 +5076,37 @@
4665 5076 wp_send_json(array('status'=>'error', 'msg'=> __('Unknown payment status','woo-vipps') . ' ' . $payment));
4666 5077 return false;
4667 5078 }
4668 5079
4669 - // The various return URLs for special pages of the Vipps stuff depend on settings and pretty-URLs so we supply them from here
4670 - // These are for the "fallback URL" mostly. IOK 2018-05-18
4671 - private function make_vipps_url($what) {
4672 - if ( !get_option('permalink_structure')) {
4673 - return add_query_arg('VippsSpecialPage', $what, home_url("/", 'https'));
4674 - }
4675 - return trailingslashit(home_url($what, 'https'));
5080 + // 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
5081 + private function make_special_page_url($action) {
5082 + return add_query_arg('action', $action, $this->get_special_page_url());
4676 5083 }
5084 +
4677 5085 public function payment_return_url() {
4678 - return apply_filters('woo_vipps_payment_return_url', $this->make_vipps_url('vipps-betaling'));
5086 + return apply_filters('woo_vipps_payment_return_url', $this->make_special_page_url('wait_for_payment'));
4679 5087 }
4680 5088 public function express_checkout_url() {
4681 - return $this->make_vipps_url('vipps-express-checkout');
5089 + return $this->make_special_page_url('do_express_checkout');
4682 5090 }
4683 5091 public function buy_product_url() {
4684 - return $this->make_vipps_url('vipps-buy-product');
5092 + return $this->make_special_page_url('buy_product');
4685 5093 }
4686 5094
4687 - // Return the method in the Vipps
4688 - public function is_special_page() {
4689 - $specials = array('vipps-betaling' => 'vipps_wait_for_payment', 'vipps-express-checkout'=>'vipps_express_checkout', 'vipps-buy-product'=>'vipps_buy_product');
4690 - $method = null;
4691 - if ( get_option('permalink_structure')) {
4692 - foreach($specials as $special=>$specialmethod) {
4693 - // IOK 2018-06-07 Change to add any prefix from home-url for better matching IOK 2018-06-07
4694 - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
4695 - if ($path && preg_match("!/$special/?$!", $path, $matches)) {
4696 - $method = $specialmethod; break;
4697 - }
4698 - }
4699 - } else {
4700 - if (isset($_GET['VippsSpecialPage'])) {
4701 - $method = @$specials[$_GET['VippsSpecialPage']];
4702 - }
4703 - }
4704 - return $method;
5095 + public static function is_special_page() {
5096 + $id = static::get_special_page_id();
5097 + return $id && is_page($id);
4705 5098 }
4706 5099
5100 + public static function get_special_page_id() {
5101 + $id = wc_get_page_id('vipps_special_page'); // -1 if not found
5102 + return $id > 0 ? $id : null;
5103 + }
5104 +
5105 + public static function get_special_page_url() {
5106 + return get_permalink(static::get_special_page_id());
5107 + }
5108 +
4707 5109 // Just create a spinner and a overlay.
4708 5110 public function spinner () {
4709 5111 $flavour = sanitize_title($this->get_payment_method_name());
4710 5112 ob_start();
@@ -4725,164 +5127,22 @@
4725 5127 return apply_filters('woo_vipps_spinner', ob_get_clean());
4726 5128 }
4727 5129
4728 5130
4729 - // Returns express logo images depending on parameters, these are the new express svgs received 2025-12-12.
4730 - // Fallbacks to defaults for each payment method. LP 2025-12-15
4731 - public function get_express_logo($payment_method, $lang, $variant) {
4732 - $base = plugins_url('img', __FILE__);
4733 -
4734 - // 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
4735 - $img_map = [
4736 - "vipps" => [
4737 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4738 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4739 - "en" => [
4740 - "default" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4741 - "default-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4742 - "buy-now-rectangular" => "$base/vipps/express/en/buy-now-vipps-en-rectangular.svg",
4743 - "buy-now-pill" => "$base/vipps/express/en/buy-now-vipps-en-pill.svg",
4744 - "express-rectangular" => "$base/vipps/express/en/express-vipps-en-rectangular.svg",
4745 - "express-rectangular-mini" => "$base/vipps/express/en/express-vipps-en-rectangular-mini.svg",
4746 - "express-pill" => "$base/vipps/express/en/express-vipps-en-pill.svg",
4747 - "express-pill-mini" => "$base/vipps/express/en/express-vipps-en-pill-mini.svg",
4748 -
4749 - ],
4750 - "no" => [
4751 - "default" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4752 - "default-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4753 - "buy-now-rectangular" => "$base/vipps/express/no/kjop-na-vipps-no-rectangular.svg",
4754 - "buy-now-pill" => "$base/vipps/express/no/kjop-na-vipps-no-pill.svg",
4755 - "express-rectangular" => "$base/vipps/express/no/ekspress-vipps-no-rectangular.svg",
4756 - "express-rectangular-mini" => "$base/vipps/express/no/ekspress-vipps-no-rectangular-mini.svg",
4757 - "express-pill" => "$base/vipps/express/no/ekspress-vipps-no-pill.svg",
4758 - "express-pill-mini" => "$base/vipps/express/no/ekspress-vipps-no-pill-mini.svg",
4759 - ],
4760 - "se" => [
4761 - "default" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4762 - "default-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4763 - "buy-now-rectangular" => "$base/vipps/express/se/kop-nu-vipps-se-rectangular.svg",
4764 - "buy-now-pill" => "$base/vipps/express/se/kop-nu-vipps-se-pill.svg",
4765 - "express-rectangular" => "$base/vipps/express/se/express-vipps-se-rectangular.svg",
4766 - "express-rectangular-mini" => "$base/vipps/express/se/express-vipps-se-rectangular-mini.svg",
4767 - "express-pill" => "$base/vipps/express/se/express-vipps-se-pill.svg",
4768 - "express-pill-mini" => "$base/vipps/express/se/express-vipps-se-pill-mini.svg",
4769 -
4770 - ],
4771 - ],
4772 - "mobilepay" => [
4773 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4774 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4775 - "en" => [
4776 - "default" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4777 - "default-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4778 - "buy-now-rectangular" => "$base/mobilepay/express/en/buy-now-mp-en-rectangular.svg",
4779 - "buy-now-pill" => "$base/mobilepay/express/en/buy-now-mp-en-pill.svg",
4780 - "express-rectangular" => "$base/mobilepay/express/en/express-mp-en-rectangular.svg",
4781 - "express-rectangular-mini" => "$base/mobilepay/express/en/express-mp-en-rectangular-mini.svg",
4782 - "express-pill" => "$base/mobilepay/express/en/express-mp-en-pill.svg",
4783 - "express-pill-mini" => "$base/mobilepay/express/en/express-mp-en-pill-mini.svg",
4784 -
4785 - ],
4786 - "dk" => [
4787 - "default" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4788 - "default-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4789 - "buy-now-rectangular" => "$base/mobilepay/express/dk/kob-nu-mp-dk-rectangular.svg",
4790 - "buy-now-pill" => "$base/mobilepay/express/dk/kob-nu-mp-dk-pill.svg",
4791 - "express-rectangular" => "$base/mobilepay/express/dk/express-mp-dk-rectangular.svg",
4792 - "express-rectangular-mini" => "$base/mobilepay/express/dk/express-mp-dk-rectangular-mini.svg",
4793 - "express-pill" => "$base/mobilepay/express/dk/express-mp-dk-pill.svg",
4794 - "express-pill-mini" => "$base/mobilepay/express/dk/express-mp-dk-pill-mini.svg",
4795 - ],
4796 - "fi" => [
4797 - "default" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4798 - "default-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4799 - "buy-now-rectangular" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-rectangular.svg",
4800 - "buy-now-pill" => "$base/mobilepay/express/fi/osta-nyt-mp-fi-pill.svg",
4801 - "express-rectangular" => "$base/mobilepay/express/fi/express-mp-fi-rectangular.svg",
4802 - "express-rectangular-mini" => "$base/mobilepay/express/fi/express-mp-fi-rectangular-mini.svg",
4803 - "express-pill" => "$base/mobilepay/express/fi/express-mp-fi-pill.svg",
4804 - "express-pill-mini" => "$base/mobilepay/express/fi/express-mp-fi-pill-mini.svg",
4805 -
4806 - ],
4807 - ],
4808 -
4809 - ];
4810 -
4811 - $payment = strtolower($payment_method);
4812 - if ($lang === 'store') $lang = $this->get_customer_language();
4813 -
4814 - // Dont give a default if payment method not found. LP 2025-12-12
4815 - if (!array_key_exists($payment, $img_map)) {
4816 - return null;
4817 - }
4818 - $payment_map = $img_map[$payment];
4819 -
4820 - $img = null;
4821 - if (array_key_exists($lang, $payment_map)
4822 - && is_array($payment_map[$lang])
4823 - && array_key_exists($variant, $payment_map[$lang])) {
4824 - $img = @$payment_map[$lang][$variant];
4825 - }
4826 -
4827 - // Default fallback behaviour
4828 - if (!$img) {
4829 - $default = str_ends_with($variant, '-mini') ? 'default-mini' : 'default';
4830 -
4831 - // First try getting default for payment method + language. LP 2026-01-16
4832 - if (array_key_exists($lang, $payment_map) && is_array($payment_map[$lang])) {
4833 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4834 - $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');
4835 - $img = @$payment_map[$lang][$default];
4836 - }
4837 -
4838 - // If not found, then try global default for payment method. LP 2026-01-16
4839 - if (!$img) {
4840 - $img = @$payment_map[$default];
4841 - }
4842 -
4843 - // Found no logo at all, log this. LP 2026-01-16
4844 - if (!$img) {
4845 - /* translators: %1= payment method name, %2 = language string, %3 = variant name */
4846 - $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');
4847 - }
4848 - }
4849 - return $img;
5131 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
5132 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5133 + public function get_express_logo($_payment_method = null, $_lang = null, $_variant = null, $context = 'global') {
5134 + return $this->get_html_button_for_context($context);
4850 5135 }
4851 5136
5137 + // DEPRECATED: Legacy function as of using new web component buttons. LP 2026-06-26
4852 5138 // Get payment logo based on payment method, then language NT 2023-11-30
4853 - // and based on custom variant setting. $page is the page origin slug, e.g 'cart', 'product'. LP 2025-12-15
4854 - public function get_payment_logo($page = null) {
4855 - $lang = $this->get_customer_language();
4856 - $payment_method = $this->get_payment_method_name();
4857 - $variant = $this->get_express_logo_page_variant($page);
4858 - $logo_url = $this->get_express_logo($payment_method, $lang, $variant);
4859 - return $logo_url;
5139 + // and based on custom variant setting. $context is where it is to be used, e.g 'cart', 'product'. LP 2025-12-15
5140 + // NB: previously this returned the url to a svg logo. We don't do this anymore, so it returns html. LP 2026-06-30
5141 + public function get_payment_logo($context = 'global') {
5142 + return $this->get_express_logo(null, null, null, $context);
4860 5143 }
4861 5144
4862 - /** Returns the correct variant to use for the given page, found from the wp option. LP 2025-12-23 */
4863 - private function get_express_logo_page_variant($page = null) {
4864 - $options = get_option('vipps_button_options');
4865 -
4866 - // Init defaults, use mini version by default in below pages. LP 2025-12-17
4867 - $use_mini = in_array($page, ['catalog']);
4868 - $variant = "";
4869 -
4870 - // Find correct variant from button settings. LP 2025-12-17
4871 - if (is_array($options) && array_key_exists('express', $options)) {
4872 - if (array_key_exists($page, $options['express']['force-mini'])) {
4873 - $use_mini = sanitize_title($options['express']['force-mini'][$page]) === 'yes';
4874 - }
4875 - $key = $use_mini ? 'mini-variant' : 'variant';
4876 - $variant = sanitize_title($options['express'][$key]) ?? '';
4877 - }
4878 -
4879 - if (!$variant) {
4880 - $variant = $use_mini ? "default-mini" : "default";
4881 - }
4882 - return apply_filters('woo_vipps_express_button_page_variant', $variant, $page);
4883 - }
4884 -
4885 5145 // Get express banner logo based on payment method. LP 2025-09-03
4886 5146 private function get_express_banner_logo() {
4887 5147 $payment_method = $this->get_payment_method_name();
4888 5148
@@ -4893,10 +5153,12 @@
4893 5153 }
4894 5154 return null;
4895 5155 }
4896 5156
4897 - // Get buy now button by manually selecting logo variant and language. LP 2026-01-16
4898 - public function get_buy_now_button_manual($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $logo_variant=null, $logo_lang=null) {
5157 + // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
5158 + // $context is slug describing where its to be used, like 'catalog', 'cart', 'product' etc. and will
5159 + // be used unless $button_args_override is nonempty. See init_button_options() and get_html_button() LP 2026-06-26
5160 + public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $context='global', $button_args_override = []) {
4899 5161 $disabled = $disabled ? 'disabled' : '';
4900 5162 $data = array();
4901 5163
4902 5164 // Support directly using the variant id as $product_id with no $variation_id. LP 2026-01-23
@@ -4911,9 +5173,8 @@
4911 5173 if ($sku) $data['product_sku'] = $sku;
4912 5174 if ($product_id) $data['product_id'] = $product_id;
4913 5175 if ($variation_id) $data['variation_id'] = $variation_id;
4914 5176
4915 -
4916 5177 $buttoncode = "<a href='javascript:void(0)' $disabled ";
4917 5178 foreach($data as $key=>$value) {
4918 5179 $value = esc_attr($value);
4919 5180 $buttoncode .= " data-$key='$value' ";
@@ -4920,14 +5181,18 @@
4920 5181 }
4921 5182
4922 5183 $payment_method = $this->get_payment_method_name();
4923 5184 $title = sprintf(__('Buy now with %1$s', 'woo-vipps'), $payment_method);
4924 - $short = str_ends_with($logo_variant, 'mini');
4925 - $logo = $this->get_express_logo($payment_method, $logo_lang, $logo_variant);
4926 5185
4927 - $message =" <img border=0 src='$logo' alt='$payment_method'/>";
5186 + if (is_array($button_args_override) && $button_args_override) {
5187 + $button_args = $button_args_override;
5188 + } else {
5189 + $button_args = $this->get_html_button_attrs_for_context($context);
5190 + }
5191 + $short = ($button_args['compact'] ?? 'false') === 'true';
5192 + $button = $this->get_html_button($button_args);
4928 5193
4929 -# Extra classes, if passed IOK 2019-02-26
5194 + # Extra classes, if passed IOK 2019-02-26
4930 5195 if (is_array($classes)) {
4931 5196 $classes = join(" ", $classes);
4932 5197 }
4933 5198 if ($classes) $classes = " $classes";
@@ -4932,19 +5197,15 @@
4932 5197 }
4933 5198 if ($classes) $classes = " $classes";
4934 5199 if ($short) $classes = "short $classes";
4935 5200
4936 - $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$message</a>";
5201 + $buttoncode .= " class='single-product button vipps-buy-now $payment_method $disabled$classes' title='$title'>$button</a>";
5202 +
5203 +
5204 +
4937 5205 return apply_filters('woo_vipps_buy_now_button', $buttoncode, $product_id, $variation_id, $sku, $disabled);
4938 5206 }
4939 5207
4940 - // Code that will generate various versions of the 'buy now with Vipps' button IOK 2018-09-27
4941 - public function get_buy_now_button($product_id,$variation_id=null,$sku=null,$disabled=false, $classes='', $page=null) {
4942 - $logo_lang = $this->get_customer_language();
4943 - $logo_variant = $this->get_express_logo_page_variant($page);
4944 - return $this->get_buy_now_button_manual($product_id, $variation_id, $sku, $disabled, $classes, $logo_variant, $logo_lang);
4945 - }
4946 -
4947 5208 // Display a 'buy now with express checkout' button on the product page IOK 2018-09-27
4948 5209 public function single_product_buy_now_button () {
4949 5210 $gw = $this->gateway();
4950 5211 $how = $gw->get_option('singleproductexpress');
@@ -5025,43 +5286,90 @@
5025 5286 }
5026 5287
5027 5288
5028 5289
5029 - // Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5290 + // Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
5030 5291 // IOK 2026-04-30 remove this when checkout is end-of-life'd
5292 + // We now also use this for the vipps special page, previously a fakepage. LP 2026-08-18
5031 5293 public function woocommerce_create_pages ($data) {
5294 + // Vipps Checkout page
5032 5295 $vipps_checkout_activated = get_option('woo_vipps_checkout_activated', false);
5033 - if (!$vipps_checkout_activated) return $data;
5296 + if ($vipps_checkout_activated) {
5297 + $data['vipps_checkout'] = array(
5298 + 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5299 + 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5300 + 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5301 + );
5302 + }
5034 5303
5035 - $data['vipps_checkout'] = array(
5036 - 'name' => _x( 'vipps_checkout', 'Page slug', 'woo-vipps' ),
5037 - 'title' => _x( 'Vipps MobilePay Checkout', 'Page title', 'woo-vipps' ),
5038 - 'content' => '<!-- wp:shortcode -->[' . 'vipps_checkout' . ']<!-- /wp:shortcode -->',
5039 - );
5040 -
5304 + // Vipps special page for certain payment flow actions. Previously a fake page. LP 2026-08-18
5305 + $data['vipps_special_page'] = [
5306 + 'name' => 'vipps-payment', // slug
5307 + /* translators: company name */
5308 + 'title' => sprintf(__('%s special page', 'woo-vipps'), static::CompanyName()), // we hide the title frontend in template_redirect. LP 2026-08-27
5309 + 'content' => '<!-- wp:shortcode -->[vipps_special_page]<!-- /wp:shortcode -->',
5310 + ];
5041 5311 return $data;
5042 5312 }
5043 5313
5044 - // Creates any necessary Vipps pages. Will be called e.g. when activating Vipps Checkout or turning it on.
5314 + // Creates any necessary Vipps pages. E.g vipps checkout page or vipps special page. LP 2026-09-01
5315 + // If a page slug already exists, then it won't overwrite or duplicate it!. LP 2026-09-02
5045 5316 public function maybe_create_vipps_pages () {
5317 + $make_pages = false;
5318 +
5319 + // Vipps Checkout page. LP 2026-08-18
5046 5320 $checkoutid = wc_get_page_id('vipps_checkout');
5047 - $makeit = !$checkoutid || ! get_post_status($checkoutid);
5048 - if ($makeit) {
5321 + if (!$checkoutid || ! get_post_status($checkoutid)) {
5049 5322 delete_option('woocommerce_vipps_checkout_page_id');
5323 + $make_pages = true;
5050 5324 }
5051 5325
5052 - if ($makeit) {
5053 - WC_Install::create_pages();
5326 + // vipps special page, previously a fake page. LP 2026-08-18
5327 + $builtin_special_page_id = static::get_special_page_id();
5328 + if (!$builtin_special_page_id || !get_post_status($builtin_special_page_id)) {
5329 + delete_option('woocommerce_vipps_special_page_page_id');
5330 + $make_pages = true;
5054 5331 }
5332 +
5333 + if ($make_pages) {
5334 + WC_Install::create_pages();
5335 + }
5055 5336 }
5056 5337
5338 + public function vipps_special_page_shortcode($atts, $content) {
5339 + // No point in expanding this unless we are actually doing the special actions. LP 2026-08-25
5340 + if (is_admin()) return;
5341 + if (wp_doing_ajax()) return;
5342 + if (defined('REST_REQUEST') && REST_REQUEST) return;
5343 + if (did_filter('woo_vipps_special_page_html')) return; // User has somehow added two shortcodes. IOK 2026-09-18
5057 5344
5345 + $action = $_GET['action'] ?? '';
5346 + $html = "";
5347 + switch ($action) {
5348 + case 'wait_for_payment':
5349 + $html = $this->vipps_wait_for_payment();
5350 + break;
5351 + case 'do_express_checkout':
5352 + $html = $this->vipps_express_checkout();
5353 + break;
5354 + case 'buy_product':
5355 + $html = $this->vipps_buy_product();
5356 + break;
5357 + default:
5358 + $html = '';
5359 + }
5360 + // This is mostly to avoid this shortcode evaluating twice IOK 2026-09-18
5361 + $html = apply_filters('woo_vipps_special_page_html', $html, $action);
5362 +
5363 + // Remember, this is a shortcode, so the html must be returned, not echoed IOK 2026-09-11
5364 + return $html;
5365 + }
5366 +
5367 +
5058 5368 // This URL will when accessed add a product to the cart and go directly to the express checkout page.
5059 5369 // The argument passed must be a shareable link created for a given product - so this in effect acts as a landing page for
5060 5370 // the buying thru Vipps Express Checkout of a single product linked to in for instance banners. IOK 2018-09-24
5061 5371 public function vipps_buy_product() {
5062 - status_header(200,'OK');
5063 - Vipps::nocache();
5064 5372
5065 5373 add_filter('body_class', function ($classes) {
5066 5374 $classes[] = 'vipps-express-checkout';
5067 5375 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
@@ -5097,9 +5405,9 @@
5097 5405
5098 5406 if (!$productinfo) {
5099 5407 $title = __("Product is no longer available",'woo-vipps');
5100 5408 $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');
5101 - return $this->fakepage($title,$content);
5409 + return $this->special_page_html($title,$content);
5102 5410 }
5103 5411
5104 5412 // Pass the productinfo to the express checkout form
5105 5413 $args = array();
@@ -5116,20 +5424,16 @@
5116 5424 }
5117 5425 $args[sanitize_title(wp_unslash($key))] = sanitize_text_field(wp_unslash($value));
5118 5426 }
5119 5427
5120 - $this->print_express_checkout_page(true,'do_single_product_express_checkout',$args);
5428 + return $this->express_checkout_page_html(true,'do_single_product_express_checkout',$args);
5121 5429 }
5122 5430
5123 - // 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.
5124 - public function vipps_express_checkout() {
5125 - status_header(200,'OK');
5126 - Vipps::nocache();
5431 + public function vipps_express_checkout_consistency_check() {
5127 5432 // We need a nonce to get here, but we should only get here when we have a cart, so this will not be cached.
5128 5433 // IOK 2018-05-28
5129 5434 $ok = isset($_REQUEST['sec']) && wp_verify_nonce($_REQUEST['sec'],'express');
5130 5435
5131 -
5132 5436 $backurl = wp_validate_redirect(@$_SERVER['HTTP_REFERER']);
5133 5437 if (!$backurl) $backurl = home_url();
5134 5438
5135 5439 if (!$ok) {
@@ -5143,8 +5447,19 @@
5143 5447 wp_redirect($backurl);
5144 5448 exit();
5145 5449 }
5146 5450
5451 + add_filter('woo_vipps_express_checkout_consistent', '__return_true');
5452 + }
5453 +
5454 + // 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.
5455 + public function vipps_express_checkout() {
5456 + // Some checks are made in template_redirect, we check here if they are ok IOK 2026-09-21
5457 + if (!apply_filters('woo_vipps_express_checkout_consistent', false)) {
5458 + $content = __('Link expired, please try again', 'woo-vipps');
5459 + return $content;
5460 + }
5461 +
5147 5462 add_filter('body_class', function ($classes) {
5148 5463 $classes[] = 'vipps-express-checkout';
5149 5464 $classes[] = 'woocommerce-checkout'; // Required by Pixel Your Site IOK 2022-11-24
5150 5465 return apply_filters('woo_vipps_express_checkout_body_class', $classes);
@@ -5151,9 +5466,9 @@
5151 5466 });
5152 5467
5153 5468 do_action('woo_vipps_express_checkout_page');
5154 5469
5155 - $this->print_express_checkout_page(true, 'do_express_checkout');
5470 + return $this->express_checkout_page_html(true, 'do_express_checkout');
5156 5471 }
5157 5472
5158 5473 // This method tries to ensure that a customer does not 'lose' the return page and
5159 5474 // starts ordering the same products twice. IOK 2020-01-22
@@ -5248,9 +5563,10 @@
5248 5563 return $orderspec;
5249 5564 }
5250 5565
5251 5566 // Used as a landing page for launching express checkout - borh for the cart and for single products. IOK 2018-09-28
5252 - protected function print_express_checkout_page($execute,$action,$productinfo=null) {
5567 + // Returns the html. LP 2026-08-27
5568 + protected function express_checkout_page_html($execute,$action,$productinfo=null) {
5253 5569 $gw = $this->gateway();
5254 5570
5255 5571 $expressCheckoutMessages = array();
5256 5572 $expressCheckoutMessages['termsAndConditionsError'] = __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' );
@@ -5261,11 +5577,12 @@
5261 5577 wp_localize_script('vipps-express-checkout', 'VippsCheckoutMessages', $expressCheckoutMessages);
5262 5578 wp_enqueue_script('vipps-express-checkout');
5263 5579 // If we have a valid nonce when we get here, just call the 'create order' bit at once. Otherwise, make a button
5264 5580 // to actually perform the express checkout.
5265 - $buttonimgurl= apply_filters('woo_vipps_express_checkout_button', $this->get_payment_logo('landing'));
5581 + $buttonhtml = apply_filters('woo_vipps_express_checkout_button', $this->get_html_button());
5266 5582
5267 5583
5584 +
5268 5585 $orderspec = $this->get_orderspec_from_arguments($productinfo);
5269 5586 if (empty($orderspec)) {
5270 5587 $orderspec = $this->get_orderspec_from_cart();
5271 5588 }
@@ -5332,10 +5649,9 @@
5332 5649
5333 5650 if ($execute) {
5334 5651 $content .= "<p id=waiting>" . __("Please wait while we are preparing your order", 'woo-vipps') . "</p>";
5335 5652 $content .= "<div id='vipps-status-message'></div>";
5336 - $this->fakepage(__('Order in progress','woo-vipps'), $content);
5337 - return;
5653 + return $this->special_page_html('', $content);
5338 5654 } else {
5339 5655 $content .= $askForConfirmationHTML;
5340 5656 $content .= $extraHTML;
5341 5657 $content .= $termsHTML;
@@ -5340,23 +5656,18 @@
5340 5656 $content .= $extraHTML;
5341 5657 $content .= $termsHTML;
5342 5658 $content .= apply_filters('woo_vipps_express_checkout_validation_elements', '');
5343 5659 $title = sprintf(__('Buy now with %1$s!', 'woo-vipps'), $this->get_payment_method_name());
5344 - $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>";
5660 + $content .= "<div class='vipps_buy_now_wrapper noloop'><a href='#' id='do-express-checkout' class='vipps-express-checkout' title='$title'>$buttonhtml</a></div>";
5345 5661 $content .= "<div id='vipps-status-message'></div>";
5346 - $this->fakepage(sprintf(__('%1$s Express Checkout','woo-vipps'), $this->get_payment_method_name()), $content);
5347 - return;
5662 + return $this->special_page_html('', $content);
5348 5663 }
5349 5664 }
5350 5665
5351 5666
5352 -
5353 - public function vipps_wait_for_payment() {
5354 - status_header(200,'OK');
5355 - Vipps::nocache();
5356 -
5667 + // Called in template_redirect before we get to the wait-for-payment page IOK 2026-09-21
5668 + private function handle_payment_poll_and_redirect () {
5357 5669 $orderid = WC()->session->get('_vipps_pending_order');
5358 -
5359 5670 $order = null;
5360 5671 $gw = $this->gateway();
5361 5672
5362 5673 // Failsafe for when the session disappears IOK 2018-11-19
@@ -5368,9 +5679,9 @@
5368 5679 // If so, we will read the order id from the GET arguments and check if the auth token is correct,
5369 5680 // simulating the session with that.
5370 5681 // IOK 2019-11-19, changed to using GET 2023-01-23
5371 5682 if ($no_session && $limited_session) {
5372 - $orderid = intval(@$_GET['id']);
5683 + $orderid = intval($_GET['id'] ?? false);
5373 5684 }
5374 5685 if ($orderid) {
5375 5686 clean_post_cache($orderid);
5376 5687 $order = wc_get_order($orderid);
@@ -5389,11 +5700,8 @@
5389 5700 $session->set('_vipps_pending_order', $orderid);
5390 5701 }
5391 5702 }
5392 5703
5393 -
5394 - do_action('woo_vipps_wait_for_payment_page',$order);
5395 -
5396 5704 $deleted_order=0;
5397 5705 if ($orderid && !$order) {
5398 5706 // If this happens, we actually did have an order, but it has been deleted, which must mean that it was cancelled.
5399 5707 // Concievably a hook on the 'cancel'-transition or in the callback handlers could clean that up before we get here. IOK 2019-09-26
@@ -5418,9 +5726,9 @@
5418 5726 clean_post_cache($orderid);
5419 5727 $order = wc_get_order($orderid); // Reload order object
5420 5728 }
5421 5729 } else {
5422 - // No need to do anyting here. IOK 2020-01-26
5730 + // No need to do anyting here. IOK 2020-01-26
5423 5731 }
5424 5732
5425 5733 $payment = 'notchecked';
5426 5734 if ($do_poll) {
@@ -5436,9 +5744,8 @@
5436 5744 exit();
5437 5745 }
5438 5746
5439 5747 // We are done, but in failure. Don't poll.
5440 - $content = "";
5441 5748 $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5442 5749
5443 5750 // Status is failed; still send to return url (as of now /order-recieved), the text there will depend on the status.
5444 5751 // For failed it shows a "Retry payment" button that takes the customer to /pay-for-order where it will be retried. LP 2026-03-17
@@ -5446,8 +5753,9 @@
5446 5753 $failure_redirect = $failure_redirect ?: $gw->get_return_url($order);
5447 5754 wp_redirect($failure_redirect);
5448 5755 exit();
5449 5756 }
5757 +
5450 5758 if ($status == 'cancelled' || $payment == 'cancelled') {
5451 5759 $this->maybe_restore_cart($orderid,'failed');
5452 5760 if ($failure_redirect){
5453 5761 wp_redirect($failure_redirect);
@@ -5452,27 +5760,45 @@
5452 5760 if ($failure_redirect){
5453 5761 wp_redirect($failure_redirect);
5454 5762 exit();
5455 5763 }
5764 + } else {
5765 + // If not, enqueue the status checker IOK 2026-09-21
5766 + 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');
5767 + }
5768 +
5769 + // Communicate this to the shortcode IOK 2026-09-21
5770 + add_filter('woo_vipps_wait_for_payment_status', function () use($orderid, $status, $payment) {
5771 + return ['orderid'=>$orderid, 'status'=>$status, 'payment'=>$payment];
5772 + });
5773 +
5774 + }
5775 +
5776 + public function vipps_wait_for_payment() {
5777 +
5778 + // This will have been computed in template_redirect, but the status will be either still pending or failed. IOK 2026-09-21
5779 + $data = apply_filters('woo_vipps_wait_for_payment_status', []);
5780 +
5781 + $orderid = $data['orderid'] ?? 0;
5782 + $status = $data['status'] ?? "";
5783 + $payment = $data['payment'] ?? "";
5784 +
5785 + $order = wc_get_order($orderid);
5786 + if (!$order) wp_die(__('Unknown order', 'woo-vipps'));
5787 +
5788 + do_action('woo_vipps_wait_for_payment_page',$order);
5789 + $gw = $this->gateway();
5790 +
5791 + $content = "";
5792 + if ($status == 'cancelled' || $payment == 'cancelled') {
5456 5793 $content .= "<div id=failure><p>". __('Order cancelled','woo-vipps') . '</p>';
5457 5794 $content .= "<p><a href='" . home_url() . "' class='btn button'>" . __('Continue shopping','woo-vipps') . '</a></p>';
5458 5795 $content .= "</div>";
5459 - $this->fakepage(__('Order cancelled','woo-vipps'), $content);
5460 -
5461 - return;
5796 + return $this->special_page_html('', $content);
5462 5797 }
5463 5798
5464 5799 // 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
5465 -
5466 5800 // Otherwise, go to a page waiting/polling for the callback. IOK 2018-05-16
5467 - 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');
5468 -
5469 - // 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
5470 - // and check that the order matches (and is 'pending') (and exists)
5471 - $vippsstamp = $order->get_meta('_vipps_init_timestamp');
5472 - $vippsstatus = $order->get_meta('_vipps_status');
5473 - $message = __($order->get_meta('_vipps_confirm_message'),'woo-vipps');
5474 -
5475 5801 $signal = $this->callbackSignal($order);
5476 5802 $content = "";
5477 5803 $content .= "<div id='waiting'><p>" . sprintf(__('Waiting for confirmation of purchase from %1$s','woo-vipps'), $this->get_payment_method_name());
5478 5804
@@ -5480,15 +5806,16 @@
5480 5806 $signalurl = $this->callbackSignalURL($signal);
5481 5807
5482 5808 $content .= "</p></div>";
5483 5809
5484 - // We impersonate the woocommerce-checkout form here mainly to work with the Pixel Your Site plugin IOK 2022-11-24
5485 - $classlist = apply_filters("woo_vipps_express_checkout_form_classes", "woocommerce-checkout");
5486 - $content .= "<form id='vippsdata' class='" . esc_attr($classlist) . "'>";
5810 + $failure_redirect = apply_filters('woo_vipps_order_failed_redirect', '', $orderid);
5811 +
5812 + // Carry the order status to the checking script IOK 2026-09-21
5813 + $content .= "<form id='vippsdata'>";
5487 5814 $content .= "<input type='hidden' id='fkey' name='fkey' value='".htmlspecialchars($signalurl)."'>";
5488 5815 $content .= "<input type='hidden' name='key' value='".htmlspecialchars($order->get_order_key())."'>";
5489 5816 $content .= "<input type='hidden' name='action' value='check_order_status'>";
5490 - $content .= wp_nonce_field('vippsstatus','sec',1,false);
5817 + $content .= wp_nonce_field('vippsstatus','sec',1,false);
5491 5818 $content .= "</form>";
5492 5819
5493 5820
5494 5821 $content .= "<div id='error' style='display:none'><p>".__('Error during order confirmation','woo-vipps'). '</p>';
@@ -5505,94 +5832,22 @@
5505 5832 $content .= "<a id='continueToOrderFailed' style='display:none' href='" . $failure_redirect . "'></a>";
5506 5833 $content .= "<a id='continueToOrderFailedFallback' style='display:none' href='" . $gw->get_return_url($order) . "'></a>";
5507 5834 $content .= "</div>";
5508 5835
5836 + return $this->special_page_html('', $content);
5837 + }
5509 5838
5510 - $this->fakepage(__('Waiting for your order confirmation','woo-vipps'), $content);
5839 + // Returns formatted html for the vipps special page. LP 2026-08-27
5840 + public function special_page_html($header, $content) {
5841 + $header_html = $header ? "<h2 class='vipps-special-page-title page-title'>$header</h2>" : '';
5842 + $html = <<<EOF
5843 + $header_html
5844 + <div class="vipps-special-page-content">$content</div>
5845 + EOF;
5846 + return apply_filters('woo_vipps_special_page_html', $html, $header, $content);
5511 5847 }
5512 5848
5513 5849
5514 -
5515 - public function fakepage($title,$content) {
5516 - global $wp, $wp_query;
5517 - // We don't want this here.
5518 - remove_filter ('the_content', 'wpautop');
5519 -
5520 - $specialid = $this->gateway()->get_option('vippsspecialpageid');
5521 - $wp_post = null;
5522 - if ($specialid) {
5523 - $wp_post = get_post($specialid);
5524 - if ($wp_post) {
5525 - $wp_post->post_title = $title;
5526 - $wp_post->post_content = $content;
5527 - // Normalize a bit
5528 - $wp_post->filter = 'raw'; // important
5529 - $wp_post->post_status = 'publish';
5530 - $wp_post->comment_status= 'closed';
5531 - $wp_post->ping_status= 'closed';
5532 - } else {
5533 - $this->log(sprintf(__("Could not use special page with id %s - it seems not to exist.", 'woo-vipps'), $specialid), 'error');
5534 - }
5535 - }
5536 - if (!$wp_post || is_wp_error($wp_post)) {
5537 - $post = new stdClass();
5538 - $post->ID = -99;
5539 - $post->post_author = 1;
5540 - $post->post_date = current_time( 'mysql' );
5541 - $post->post_date_gmt = current_time( 'mysql', 1 );
5542 - $post->post_title = $title;
5543 - $post->post_content = $content;
5544 - $post->post_status = 'publish';
5545 - $post->comment_status = 'closed';
5546 - $post->ping_status = 'closed';
5547 - $post->post_name = 'vippsconfirm-fake-page-name';
5548 - $post->post_type = 'page';
5549 - $post->filter = 'raw'; // important
5550 - $wp_post = new WP_Post($post);
5551 - wp_cache_add( -99, $wp_post, 'posts' );
5552 - }
5553 -
5554 - // Update the main query
5555 - $wp_query->post = $wp_post;
5556 - $wp_query->posts = array( $wp_post );
5557 - $wp_query->queried_object = $wp_post;
5558 - $wp_query->queried_object_id = $wp_post->ID;
5559 - $wp_query->found_posts = 1;
5560 - $wp_query->post_count = 1;
5561 - $wp_query->max_num_pages = 1;
5562 - $wp_query->is_page = true;
5563 - $wp_query->is_singular = true;
5564 - $wp_query->is_single = false;
5565 - $wp_query->is_attachment = false;
5566 - $wp_query->is_archive = false;
5567 - $wp_query->is_category = false;
5568 - $wp_query->is_tag = false;
5569 - $wp_query->is_tax = false;
5570 - $wp_query->is_author = false;
5571 - $wp_query->is_date = false;
5572 - $wp_query->is_year = false;
5573 - $wp_query->is_month = false;
5574 - $wp_query->is_day = false;
5575 - $wp_query->is_time = false;
5576 - $wp_query->is_search = false;
5577 - $wp_query->is_feed = false;
5578 - $wp_query->is_comment_feed = false;
5579 - $wp_query->is_trackback = false;
5580 - $wp_query->is_home = false;
5581 - $wp_query->is_embed = false;
5582 - $wp_query->is_404 = false;
5583 - $wp_query->is_paged = false;
5584 - $wp_query->is_admin = false;
5585 - $wp_query->is_preview = false;
5586 - $wp_query->is_robots = false;
5587 - $wp_query->is_posts_page = false;
5588 - $wp_query->is_post_type_archive = false;
5589 - // Update globals
5590 - $GLOBALS['wp_query'] = $wp_query;
5591 - $wp->register_globals();
5592 - return $wp_post;
5593 - }
5594 -
5595 5850 // Support the interactivity API with data about our cart IOK 2026-02-23
5596 5851 public function woo_vipps_store_api_cart_data() {
5597 5852 // 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
5598 5853
@@ -5598,13 +5853,15 @@
5598 5853
5599 5854 $checkout_page = $this->gateway()->vipps_checkout_available();
5600 5855 $standard_checkout = get_permalink(get_option('woocommerce_checkout_page_id'));
5601 5856 $checkout_url = $checkout_page ? get_permalink($checkout_page) : $standard_checkout;
5857 +
5602 5858 $cart_data = array(
5603 5859 'cart_hide_express' => !$this->gateway()->show_express_checkout(),
5604 5860 'cart_supports_checkout' => (bool) $checkout_page,
5605 5861 'checkout_url' => $checkout_url,
5606 5862 );
5863 +
5607 5864 return $cart_data;
5608 5865 }
5609 5866
5610 5867 public function woo_vipps_store_api_cart_schema() {
@@ -5626,8 +5883,117 @@
5626 5883 ),
5627 5884 );
5628 5885 }
5629 5886
5887 + // Inits option 'vipps_button_options' and handles migration from older versions. LP 2026-06-26
5888 + // new version is stored as vipps_button_options2 to avoid breaking older versions on version revert. IOK 2026-07-15
5889 + private function init_button_options() {
5890 + /* New structure as of now
5891 + * [
5892 + * 'version' => x.x,
5893 + * 'express' => [
5894 + * 'version' => x.x, used to migrate from previous iterations
5895 + * 'configs' => [ different button parameters for certain contexts, falls back to global if context has no override
5896 + * 'global' => ['compact' => ..., 'verb' => ..., ...],
5897 + * 'cart' => [...],
5898 + * 'product' => [...],
5899 + * 'checkout' => [...],
5900 + * ...
5901 + * ],
5902 + * 'product_configs' => [ overrides for specific products
5903 + * 1532 => ['compact' => ..., 'verb' => ..., ...],
5904 + * ...
5905 + * ],
5906 + * ],
5907 + * ]
5908 + */
5909 + $options = get_option('vipps_button_options2');
5910 + if (!empty($options)) return;
5911 +
5912 + $old_options = get_option('vipps_button_options');
5913 + $default_config = $this->get_html_button_default_attrs();
5914 + unset($default_config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5915 + $default_compact = array_replace($default_config, ['compact' => 'true']);
5916 +
5917 + $default_options = [
5918 + 'version' => $this->button_options_version,
5919 + 'express' => [
5920 + 'version' => $this->button_options_express_version,
5921 + 'configs' => [
5922 + 'global' => $default_config,
5923 + // Need compact version by default for below pages. LP 2026-07-07
5924 + 'catalog' => $default_compact,
5925 + 'minicart' => $default_compact, // storefront needs compact, tho 2025 theme has a lot of room. Just use compact LP 2026-07-07
5926 + ],
5927 + 'product_configs' => [],
5928 + ],
5929 + ];
5930 +
5931 + $new_options = $default_options;
5932 +
5933 + //Actually, we have some options from the old structure IOK 2026-07-15
5934 + if (!empty($old_options)) {
5935 + $new_options['express']['configs']['global'] = $this->migrate_button_variant_to_config($old_options['express']['variant'] ?? '');
5936 + unset($new_options['express']['configs']['global']['brand']); // dont set brand, this needs to be dynamic. LP 2026-07-01
5937 + }
5938 +
5939 + // Migrate context/page mini override to new context config. LP 2026-06-26
5940 + if (is_array($old_options['express']['force-mini'] ?? null)) {
5941 + foreach($old_options['express']['force-mini'] as $context => $use_mini) {
5942 + if ("yes" === $use_mini) {
5943 + $config = $this->migrate_button_variant_to_config($old_options['express']['mini-variant'] ?? '');
5944 + unset($config['brand']); // brand needs to be dynamic from payment method! LP 2026-07-01
5945 + $config['compact'] = 'true';
5946 + $new_options['express']['configs'][$context] = $config;
5947 + }
5948 + }
5949 + }
5950 +
5951 + if ($this->get_payment_method_name() !== 'MobilePay') {
5952 + // Finnish is only available in the MobilePay component right now, so reset language in any configs. LP 2026-07-01
5953 + foreach(($new_options['express']['configs'] ?? []) as $context => $config) {
5954 + if ('fi' === ($config['language'] ?? '')) {
5955 + $config['language'] = 'store';
5956 + $new_options['express']['configs'][$context] = $config;
5957 + }
5958 + }
5959 + }
5960 +
5961 + /* translators: placeholders are arrays */
5962 + $this->log(sprintf(__('Migrating from old button options. Old: %s, new: %s', 'woo-vipps'), print_r($options, true), print_r($new_options, true)), 'debug');
5963 +
5964 +
5965 +
5966 + update_option('vipps_button_options2', $new_options);
5967 + }
5968 +
5969 + // Old variant string => new config array. LP 2026-06-26
5970 + public function migrate_button_variant_to_config($variant_slug) {
5971 + if (!is_string($variant_slug)) return [];
5972 + $config = $this->get_html_button_default_attrs();
5973 + $config['rounded'] = str_contains($variant_slug, 'pill') ? 'true' : 'false';
5974 + $config['compact'] = str_contains($variant_slug, 'mini') ? 'true' : 'false';
5975 + if (str_contains($variant_slug, 'buy-now')) {
5976 + $config['verb'] = 'buy';
5977 + } else if (str_contains($variant_slug, 'express')) {
5978 + $config['verb'] = 'express';
5979 + }
5980 + return $config;
5981 + }
5982 +
5983 + // Old legacy button logo variants. Replaced by web component. See get_html_button(). LP 2026-07-01
5984 + public function get_express_logo_variants() {
5985 + return [
5986 + 'buy-now-rectangular' => __('Buy now rectangular', 'woo-vipps'),
5987 + 'buy-now-pill' => __('Buy now pill', 'woo-vipps'),
5988 + 'express-rectangular' => __('Express rectangular', 'woo-vipps'),
5989 + 'express-pill' => __('Express pill', 'woo-vipps'),
5990 + 'express-rectangular-mini' => __('Express rectangular mini', 'woo-vipps'),
5991 + 'express-pill-mini' => __('Express pill mini', 'woo-vipps'),
5992 + ];
5993 + }
5994 +
5995 +
5630 5996 // Whether the order is possible to restart with a retry session at VMP. LP 2026-03-18
5631 5997 public static function order_is_vipps_retryable($order_id) {
5632 5998 $order = wc_get_order($order_id);
5633 5999 if (!$order) return false;
@@ -5636,6 +6002,20 @@
5636 6002 $shipping_set = $order->get_meta('_vipps_shipping_set');
5637 6003
5638 6004 // Express or unfinalized Checkout orders do not have shipping available, so we cant retry these in particular. LP 2026-03-18
5639 6005 return $nonexpress_epayment || $shipping_set;
6006 + }
6007 +
6008 + /** Returns the plugin's rest api namespace including the version.
6009 + * Use latest version ($version = 'latest') with caution, we want backwards compatible endpoints. LP 2026-03-31 */
6010 + public static function get_rest_namespace($version = 'latest') {
6011 + $version = $version === 'latest' ? self::REST_CURRENT_VERSION : $version;
6012 + return self::REST_NAMESPACE_BASE . "/$version";
6013 + }
6014 +
6015 + /** Returns the plugin's rest api url.
6016 + * $version accepts 'latest', but you probably don't want to do that.
6017 + * Remember root forward-slash for $route. e.g $route = '/my-route' LP 2026-03-31 */
6018 + public static function get_rest_url($version, $route) {
6019 + return get_rest_url(null, static::get_rest_namespace($version) . $route, 'rest');
5640 6020 }
5641 6021 }